diff options
-rw-r--r-- | Timeline.Tests/Helpers/TestClock.cs | 2 | ||||
-rw-r--r-- | Timeline.Tests/Services/TimelineServiceTest.cs | 34 | ||||
-rw-r--r-- | Timeline/Resources/Services/TimelineService.Designer.cs | 9 | ||||
-rw-r--r-- | Timeline/Resources/Services/TimelineService.resx | 3 | ||||
-rw-r--r-- | Timeline/Services/TimelineService.cs | 77 |
5 files changed, 101 insertions, 24 deletions
diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs index de7d0eb7..0cbf236d 100644 --- a/Timeline.Tests/Helpers/TestClock.cs +++ b/Timeline.Tests/Helpers/TestClock.cs @@ -36,7 +36,7 @@ namespace Timeline.Tests.Helpers {
if (_currentTime == null)
return SetMockCurrentTime();
- _currentTime.Value.Add(timeSpan);
+ _currentTime += timeSpan;
return _currentTime.Value;
}
}
diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs index cb2ade61..4f081b5d 100644 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/Timeline.Tests/Services/TimelineServiceTest.cs @@ -63,11 +63,11 @@ namespace Timeline.Tests.Services [InlineData("tl")]
public async Task Timeline_LastModified(string timelineName)
{
- _clock.ForwardCurrentTime();
+ var initTime = _clock.ForwardCurrentTime();
void Check(Models.Timeline timeline)
{
- timeline.NameLastModified.Should().Be(_clock.GetCurrentTime());
+ timeline.NameLastModified.Should().Be(initTime);
timeline.LastModified.Should().Be(_clock.GetCurrentTime());
}
@@ -90,5 +90,35 @@ namespace Timeline.Tests.Services await _timelineService.ChangeMember(timelineName, new List<string> { "admin" }, null);
await GetAndCheck();
}
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince(string timelineName)
+ {
+ _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ DateTime testPoint = new DateTime();
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ var t = _clock.ForwardCurrentTime();
+ if (index == 1)
+ testPoint = t;
+ await _timelineService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var posts = await _timelineService.GetPosts(timelineName, testPoint);
+ posts.Should().HaveCount(2)
+ .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(2));
+ }
}
}
diff --git a/Timeline/Resources/Services/TimelineService.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs index cfc381f9..e16c1337 100644 --- a/Timeline/Resources/Services/TimelineService.Designer.cs +++ b/Timeline/Resources/Services/TimelineService.Designer.cs @@ -106,6 +106,15 @@ namespace Timeline.Resources.Services { }
/// <summary>
+ /// Looks up a localized string similar to The post has been deleted because content of entity is null..
+ /// </summary>
+ internal static string ExceptionPostDeleted {
+ get {
+ return ResourceManager.GetString("ExceptionPostDeleted", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The timeline name is of bad format..
/// </summary>
internal static string ExceptionTimelineNameBadFormat {
diff --git a/Timeline/Resources/Services/TimelineService.resx b/Timeline/Resources/Services/TimelineService.resx index c4f49b30..9314f51b 100644 --- a/Timeline/Resources/Services/TimelineService.resx +++ b/Timeline/Resources/Services/TimelineService.resx @@ -141,4 +141,7 @@ <data name="LogGetDataNoFormat" xml:space="preserve">
<value>Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it.</value>
</data>
+ <data name="ExceptionPostDeleted" xml:space="preserve">
+ <value>The post has been deleted because content of entity is null.</value>
+ </data>
</root>
\ No newline at end of file diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 6c1e91c6..e0b1b51d 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -106,6 +106,20 @@ namespace Timeline.Services Task<List<TimelinePost>> GetPosts(string timelineName);
/// <summary>
+ /// Get the posts that have been modified since a given time in the timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="modifiedSince">The time that posts have been modified since.</param>
+ /// <returns>A list of all posts.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<List<TimelinePost>> GetPosts(string timelineName, DateTime modifiedSince);
+
+ /// <summary>
/// Get the etag of data of a post.
/// </summary>
/// <param name="timelineName">The name of the timeline of the post.</param>
@@ -383,6 +397,34 @@ namespace Timeline.Services };
}
+ private async Task<TimelinePost> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
+ {
+ if (entity.Content == null)
+ {
+ throw new ArgumentException(ExceptionPostDeleted, nameof(entity));
+ }
+
+ var author = await _userService.GetUserById(entity.AuthorId);
+
+ var type = entity.ContentType;
+
+ ITimelinePostContent content = type switch
+ {
+ TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
+ TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
+ _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
+ };
+
+ return new TimelinePost(
+ id: entity.LocalId,
+ author: author,
+ content: content,
+ time: entity.Time,
+ lastUpdated: entity.LastUpdated,
+ timelineName: timelineName
+ );
+ }
+
private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId)
{
var currentTime = _clock.GetCurrentTime();
@@ -488,30 +530,23 @@ namespace Timeline.Services var posts = new List<TimelinePost>();
foreach (var entity in postEntities)
{
- if (entity.Content == null)
- {
- throw new Exception();
- }
-
- var author = await _userService.GetUserById(entity.AuthorId);
+ posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
+ }
+ return posts;
+ }
- var type = entity.ContentType;
+ public async Task<List<TimelinePost>> GetPosts(string timelineName, DateTime modifiedSince)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
- ITimelinePostContent content = type switch
- {
- TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
- TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
- _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
- };
+ var timelineId = await FindTimelineId(timelineName);
+ var postEntities = await _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null && p.LastUpdated > modifiedSince).ToListAsync();
- posts.Add(new TimelinePost(
- id: entity.LocalId,
- author: author,
- content: content,
- time: entity.Time,
- lastUpdated: entity.LastUpdated,
- timelineName: timelineName
- ));
+ var posts = new List<TimelinePost>();
+ foreach (var entity in postEntities)
+ {
+ posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
}
return posts;
}
|