From 26b94a034c5a91b3a903b91c3c61acbbf78db401 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 18 Dec 2020 18:01:25 +0800 Subject: feat: Add bookmark timeline service interface. --- .../Timeline/Services/BookmarkTimelineService.cs | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 BackEnd/Timeline/Services/BookmarkTimelineService.cs (limited to 'BackEnd/Timeline/Services/BookmarkTimelineService.cs') diff --git a/BackEnd/Timeline/Services/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/BookmarkTimelineService.cs new file mode 100644 index 00000000..7eb691b7 --- /dev/null +++ b/BackEnd/Timeline/Services/BookmarkTimelineService.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + + [Serializable] + public class InvalidBookmarkException : Exception + { + public InvalidBookmarkException() { } + public InvalidBookmarkException(string message) : base(message) { } + public InvalidBookmarkException(string message, Exception inner) : base(message, inner) { } + protected InvalidBookmarkException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } + + /// + /// Service interface that manages timeline bookmarks. + /// + public interface IBookmarkTimelineService + { + /// + /// Get bookmarks of a user. + /// + /// User id of bookmark owner. + /// Bookmarks in order. + /// Thrown when user does not exist. + Task> GetBookmarks(long userId); + + /// + /// Add a bookmark to tail to a user. + /// + /// User id of bookmark owner. + /// Timeline name. + /// Thrown when is null. + /// Thrown when is not a valid name. + /// Thrown when user does not exist. + /// Thrown when timeline does not exist. + Task AddBookmark(long userId, string timelineName); + + /// + /// Remove a bookmark from a user. + /// + /// User id of bookmark owner. + /// Timeline name. + /// True if deletion is performed. False if bookmark does not exist. + /// Thrown when is null. + /// Thrown when is not a valid name. + /// Thrown when user does not exist. + /// Thrown when timeline does not exist. + Task RemoveBookmark(long userId, string timelineName); + + /// + /// Move bookmark to a new position. + /// + /// User id of bookmark owner. + /// Timeline name. + /// New position. Starts at 1. + /// Thrown when is null. + /// Thrown when is not a valid name. + /// Thrown when user does not exist. + /// Thrown when timeline does not exist. + /// Thrown when the timeline is not a bookmark. + Task MoveBookmark(long userId, string timelineName, long position); + } + + public class BookmarkTimelineService + { + } +} -- cgit v1.2.3 From 61630cf39af0f74f102f772ed0f3ca4a73731e11 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 18 Dec 2020 19:26:50 +0800 Subject: feat: Implement bookmark service. --- BackEnd/Timeline/Entities/DatabaseContext.cs | 1 + .../Timeline/Services/BookmarkTimelineService.cs | 127 ++++++++++++++++++++- 2 files changed, 124 insertions(+), 4 deletions(-) (limited to 'BackEnd/Timeline/Services/BookmarkTimelineService.cs') diff --git a/BackEnd/Timeline/Entities/DatabaseContext.cs b/BackEnd/Timeline/Entities/DatabaseContext.cs index 4205c2cf..513cdc95 100644 --- a/BackEnd/Timeline/Entities/DatabaseContext.cs +++ b/BackEnd/Timeline/Entities/DatabaseContext.cs @@ -30,6 +30,7 @@ namespace Timeline.Entities public DbSet TimelinePosts { get; set; } = default!; public DbSet TimelineMembers { get; set; } = default!; public DbSet HighlightTimelines { get; set; } = default!; + public DbSet BookmarkTimelines { get; set; } = default!; public DbSet JwtToken { get; set; } = default!; public DbSet Data { get; set; } = default!; diff --git a/BackEnd/Timeline/Services/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/BookmarkTimelineService.cs index 7eb691b7..f65a1ff0 100644 --- a/BackEnd/Timeline/Services/BookmarkTimelineService.cs +++ b/BackEnd/Timeline/Services/BookmarkTimelineService.cs @@ -1,6 +1,9 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Timeline.Entities; using Timeline.Models; using Timeline.Services.Exceptions; @@ -59,16 +62,132 @@ namespace Timeline.Services /// /// User id of bookmark owner. /// Timeline name. - /// New position. Starts at 1. + /// New position. Starts at 1. /// Thrown when is null. /// Thrown when is not a valid name. /// Thrown when user does not exist. /// Thrown when timeline does not exist. /// Thrown when the timeline is not a bookmark. - Task MoveBookmark(long userId, string timelineName, long position); + Task MoveBookmark(long userId, string timelineName, long newPosition); } - public class BookmarkTimelineService + public class BookmarkTimelineService : IBookmarkTimelineService { + private readonly DatabaseContext _database; + private readonly IBasicUserService _userService; + private readonly ITimelineService _timelineService; + + public BookmarkTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService) + { + _database = database; + _userService = userService; + _timelineService = timelineService; + } + + public async Task AddBookmark(long userId, string timelineName) + { + if (timelineName is null) + throw new ArgumentNullException(nameof(timelineName)); + + if (!await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + var timelineId = await _timelineService.GetTimelineIdByName(timelineName); + + _database.BookmarkTimelines.Add(new BookmarkTimelineEntity + { + TimelineId = timelineId, + UserId = userId, + Rank = (await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId)) + 1 + }); + + await _database.SaveChangesAsync(); + } + + public async Task> GetBookmarks(long userId) + { + if (!await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).Select(t => new { t.TimelineId }).ToListAsync(); + + List result = new(); + + foreach (var entity in entities) + { + result.Add(await _timelineService.GetTimelineById(entity.TimelineId)); + } + + return result; + } + + public async Task MoveBookmark(long userId, string timelineName, long newPosition) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _timelineService.GetTimelineIdByName(timelineName); + + var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId && t.UserId == userId); + + if (entity == null) throw new InvalidBookmarkException("You can't move a non-bookmark timeline."); + + var oldPosition = entity.Rank; + + if (newPosition < 1) + { + newPosition = 1; + } + else + { + var totalCount = await _database.HighlightTimelines.CountAsync(); + if (newPosition > totalCount) newPosition = totalCount; + } + + if (oldPosition == newPosition) return; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + if (newPosition > oldPosition) + { + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", oldPosition + 1, newPosition, userId); + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); + } + else + { + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` + 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", newPosition, oldPosition - 1, userId); + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); + } + + await transaction.CommitAsync(); + } + + public async Task RemoveBookmark(long userId, string timelineName) + { + if (timelineName is null) + throw new ArgumentNullException(nameof(timelineName)); + + if (!await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + var timelineId = await _timelineService.GetTimelineIdByName(timelineName); + + var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.UserId == userId && t.TimelineId == timelineId); + + if (entity == null) return false; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + var rank = entity.Rank; + + _database.BookmarkTimelines.Remove(entity); + await _database.SaveChangesAsync(); + + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` > {0}", rank); + + await transaction.CommitAsync(); + + return true; + } } } -- cgit v1.2.3 From b102ed3edf1330e77240708afb59ec8f91d67b41 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 18 Dec 2020 22:40:22 +0800 Subject: feat: Bookmark timeline service unit tests. --- .../Services/BookmarkTimelineServiceTest.cs | 89 ++++++++++++++++++++++ .../Services/HighlightTimelineServiceTest.cs | 4 + .../Timeline/Services/BookmarkTimelineService.cs | 4 +- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs (limited to 'BackEnd/Timeline/Services/BookmarkTimelineService.cs') diff --git a/BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs new file mode 100644 index 00000000..1b8bff63 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs @@ -0,0 +1,89 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class BookmarkTimelineServiceTest : DatabaseBasedTest + { + private BookmarkTimelineService _service = default!; + private UserService _userService = default!; + private TimelineService _timelineService = default!; + + protected override void OnDatabaseCreated() + { + var clock = new TestClock(); + _userService = new UserService(NullLogger.Instance, Database, new PasswordService(), new UserPermissionService(Database), clock); + _timelineService = new TimelineService(Database, _userService, clock); + _service = new BookmarkTimelineService(Database, _userService, _timelineService); + } + + [Fact] + public async Task Should_Work() + { + var userId = await _userService.GetUserIdByUsername("user"); + + { + var b = await _service.GetBookmarks(userId); + b.Should().BeEmpty(); + } + + await _timelineService.CreateTimeline("tl", userId); + await _service.AddBookmark(userId, "tl"); + + { + var b = await _service.GetBookmarks(userId); + b.Should().HaveCount(1).And.BeEquivalentTo(await _timelineService.GetTimeline("tl")); + } + } + + [Fact] + public async Task NewOne_Should_BeAtLast() + { + var userId = await _userService.GetUserIdByUsername("user"); + await _timelineService.CreateTimeline("t1", userId); + await _service.AddBookmark(userId, "t1"); + + await _timelineService.CreateTimeline("t2", userId); + await _service.AddBookmark(userId, "t2"); + + var b = await _service.GetBookmarks(userId); + + b.Should().HaveCount(2); + b[0].Name.Should().Be("t1"); + b[1].Name.Should().Be("t2"); + } + + [Fact] + public async Task Multiple_Should_Work() + { + var userId = await _userService.GetUserIdByUsername("user"); + + // make timeline id not same as entity id. + await _timelineService.CreateTimeline("t0", userId); + + await _timelineService.CreateTimeline("t1", userId); + await _service.AddBookmark(userId, "t1"); + + await _timelineService.CreateTimeline("t2", userId); + await _service.AddBookmark(userId, "t2"); + + await _timelineService.CreateTimeline("t3", userId); + await _service.AddBookmark(userId, "t3"); + + await _service.MoveBookmark(userId, "t3", 2); + (await _service.GetBookmarks(userId))[1].Name.Should().Be("t3"); + + await _service.MoveBookmark(userId, "t1", 3); + (await _service.GetBookmarks(userId))[2].Name.Should().Be("t1"); + + await _service.RemoveBookmark(userId, "t2"); + await _service.RemoveBookmark(userId, "t1"); + await _service.RemoveBookmark(userId, "t3"); + (await _service.GetBookmarks(userId)).Should().BeEmpty(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs index dca070c6..f48404a9 100644 --- a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -68,6 +68,10 @@ namespace Timeline.Tests.Services public async Task Multiple_Should_Work() { var userId = await _userService.GetUserIdByUsername("user"); + + // make timeline id not same as entity id. + await _timelineService.CreateTimeline("t0", userId); + await _timelineService.CreateTimeline("t1", userId); await _service.AddHighlightTimeline("t1", userId); diff --git a/BackEnd/Timeline/Services/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/BookmarkTimelineService.cs index f65a1ff0..09438193 100644 --- a/BackEnd/Timeline/Services/BookmarkTimelineService.cs +++ b/BackEnd/Timeline/Services/BookmarkTimelineService.cs @@ -109,7 +109,7 @@ namespace Timeline.Services if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); - var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).Select(t => new { t.TimelineId }).ToListAsync(); + var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).OrderBy(t => t.Rank).Select(t => new { t.TimelineId }).ToListAsync(); List result = new(); @@ -140,7 +140,7 @@ namespace Timeline.Services } else { - var totalCount = await _database.HighlightTimelines.CountAsync(); + var totalCount = await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId); if (newPosition > totalCount) newPosition = totalCount; } -- cgit v1.2.3