From 66658abde1220a53d0e022aaac8dd49a15034a34 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 4 Feb 2021 22:03:08 +0800 Subject: ... --- .../Timeline/Controllers/TimelinePostController.cs | 39 +++++++++++++++++++--- BackEnd/Timeline/Models/Mapper/TimelineMapper.cs | 4 +-- BackEnd/Timeline/Services/TimelinePostService.cs | 30 +++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) (limited to 'BackEnd/Timeline') diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index 3f31decf..0148f56e 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -53,7 +53,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> PostList([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) + public async Task>> List([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); @@ -68,6 +68,37 @@ namespace Timeline.Controllers return result; } + /// + /// Get a post of a timeline. + /// + /// The name of the timeline. + /// The post id. + /// The post. + [HttpGet("{post}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get([FromRoute][GeneralTimelineName] string timeline, [FromRoute(Name = "post")] long postId) + { + var timelineId = await _timelineService.GetTimelineIdByName(timeline); + + if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + var post = await _postService.GetPost(timelineId, postId); + var result = await _timelineMapper.MapToHttp(post, timeline, Url); + return result; + } + catch (TimelinePostNotExistException) + { + return NotFound(ErrorResponse.TimelineController.PostNotExist()); + } + } + /// /// Get the data of a post. Usually a image post. /// @@ -82,7 +113,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task PostDataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) + public async Task DataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) { _ = ifNoneMatch; @@ -121,7 +152,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> PostPost([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) + public async Task> Post([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); var userId = this.GetUserId(); @@ -193,7 +224,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task PostDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) + public async Task Delete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); diff --git a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs index 94e55237..88c96d8a 100644 --- a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs +++ b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs @@ -49,7 +49,7 @@ namespace Timeline.Models.Mapper isBookmark: userId is not null && await _bookmarkTimelineService.IsBookmark(userId.Value, entity.Id, false, false), links: new HttpTimelineLinks( self: urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName }), - posts: urlHelper.ActionLink(nameof(TimelinePostController.PostList), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName }) + posts: urlHelper.ActionLink(nameof(TimelinePostController.List), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName }) ) ); } @@ -84,7 +84,7 @@ namespace Timeline.Models.Mapper ( type: TimelinePostContentTypes.Image, text: null, - url: urlHelper.ActionLink(nameof(TimelinePostController.PostDataGet), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }), + url: urlHelper.ActionLink(nameof(TimelinePostController.DataGet), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }), eTag: $"\"{entity.Content}\"" ), _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, "Unknown timeline post type {0}.", entity.ContentType)) diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index c2b773ff..cf5f4e55 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -44,6 +44,17 @@ namespace Timeline.Services /// Thrown when timeline does not exist. Task> GetPosts(long timelineId, DateTime? modifiedSince = null, bool includeDeleted = false); + /// + /// Get a post of a timeline. + /// + /// The id of the timeline of the post. + /// The id of the post. + /// If true, return the entity even if it is deleted. + /// The post. + /// Thrown when timeline does not exist. + /// Thrown when post of does not exist or has been deleted. + Task GetPost(long timelineId, long postId, bool includeDelete = false); + /// /// Get the etag of data of a post. /// @@ -189,6 +200,25 @@ namespace Timeline.Services return await query.ToListAsync(); } + public async Task GetPost(long timelineId, long postId, bool includeDelete = false) + { + await CheckTimelineExistence(timelineId); + + var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (post is null) + { + throw new TimelinePostNotExistException(timelineId, postId, false); + } + + if (!includeDelete && post.Content is null) + { + throw new TimelinePostNotExistException(timelineId, postId, true); + } + + return post; + } + public async Task GetPostDataETag(long timelineId, long postId) { await CheckTimelineExistence(timelineId); -- cgit v1.2.3 From d42d826ae863b20913321d229a168b05d723db73 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 6 Feb 2021 16:41:19 +0800 Subject: ... --- .../Http/HttpTimelinePostCreateRequestContent.cs | 2 ++ .../Models/Http/HttpTimelinePostPatchRequest.cs | 39 ++++++++++++++++++++++ BackEnd/Timeline/Models/Timeline.cs | 24 ------------- .../Timeline/Models/TimelinePostContentTypes.cs | 11 ++++++ BackEnd/Timeline/Models/TimelineVisibility.cs | 18 ++++++++++ .../Validation/TimelinePostContentTypeValidator.cs | 18 ++++++++++ 6 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs delete mode 100644 BackEnd/Timeline/Models/Timeline.cs create mode 100644 BackEnd/Timeline/Models/TimelinePostContentTypes.cs create mode 100644 BackEnd/Timeline/Models/TimelineVisibility.cs create mode 100644 BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs (limited to 'BackEnd/Timeline') diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs index f4b300a9..12ab407f 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; namespace Timeline.Models.Http { @@ -11,6 +12,7 @@ namespace Timeline.Models.Http /// Type of post content. /// [Required] + [TimelinePostContentType] public string Type { get; set; } = default!; /// /// If post is of text type, this is the text. diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs new file mode 100644 index 00000000..3dface29 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs @@ -0,0 +1,39 @@ +using System; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// The model of changing post content. + /// + public class HttpTimelinePostPatchRequestContent + { + /// + /// The new type of the post. If null, old type is used. This field can't be used alone. Use it with corresponding fields to change post content. + /// + [TimelinePostContentType] + public string? Type { get; set; } + /// + /// The new text. Null for not change. + /// + public string? Text { get; set; } + /// + /// The new data. Null for not change. + /// + public string? Data { get; set; } + } + + public class HttpTimelinePostPatchRequest + { + /// + /// Change the time. Null for not change. + /// + public DateTime? Time { get; set; } + + /// + /// Change the color. Null for not change. + /// + [Color] + public string? Color { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/Timeline.cs b/BackEnd/Timeline/Models/Timeline.cs deleted file mode 100644 index 9f3eabdf..00000000 --- a/BackEnd/Timeline/Models/Timeline.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Timeline.Models -{ - public enum TimelineVisibility - { - /// - /// All people including those without accounts. - /// - Public, - /// - /// Only people signed in. - /// - Register, - /// - /// Only member. - /// - Private - } - - public static class TimelinePostContentTypes - { - public const string Text = "text"; - public const string Image = "image"; - } -} diff --git a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs new file mode 100644 index 00000000..22763eba --- /dev/null +++ b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Timeline.Models +{ + public static class TimelinePostContentTypes + { + public static string[] AllTypes { get; } = new string[] { Text, Image }; + public const string Text = "text"; + public const string Image = "image"; + } +} diff --git a/BackEnd/Timeline/Models/TimelineVisibility.cs b/BackEnd/Timeline/Models/TimelineVisibility.cs new file mode 100644 index 00000000..7c1e309b --- /dev/null +++ b/BackEnd/Timeline/Models/TimelineVisibility.cs @@ -0,0 +1,18 @@ +namespace Timeline.Models +{ + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } +} diff --git a/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs b/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs new file mode 100644 index 00000000..483cce06 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs @@ -0,0 +1,18 @@ +using System; + +namespace Timeline.Models.Validation +{ + public class TimelinePostContentTypeValidator : StringSetValidator + { + public TimelinePostContentTypeValidator() : base(TimelinePostContentTypes.AllTypes) { } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + public class TimelinePostContentTypeAttribute : ValidateWithAttribute + { + public TimelinePostContentTypeAttribute() : base(typeof(TimelinePostContentTypeValidator)) + { + + } + } +} -- cgit v1.2.3 From 204cb9a59494471804fe17a4a15f6856b531bb54 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 6 Feb 2021 17:43:03 +0800 Subject: ... --- .../Timeline/Controllers/TimelinePostController.cs | 77 ++++++----- .../Timeline/Models/TimelinePostContentTypes.cs | 3 + BackEnd/Timeline/Services/TimelinePostService.cs | 141 ++++++++++++--------- 3 files changed, 118 insertions(+), 103 deletions(-) (limited to 'BackEnd/Timeline') diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index 0148f56e..4dab2a44 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -162,54 +162,51 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - var content = body.Content; + var requestContent = body.Content; - TimelinePostEntity post; + TimelinePostCreateRequestContent createContent; - TimelinePostCommonProperties properties = new TimelinePostCommonProperties { Color = body.Color, Time = body.Time }; - - if (content.Type == TimelinePostContentTypes.Text) + switch (requestContent.Type) { - var text = content.Text; - if (text == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); - } - post = await _postService.CreateTextPost(timelineId, userId, text, properties); + case TimelinePostContentTypes.Text: + if (requestContent.Text is null) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); + } + createContent = new TimelinePostCreateRequestTextContent(requestContent.Text); + break; + case TimelinePostContentTypes.Image: + if (requestContent.Data is null) + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); + + // decode base64 + byte[] data; + try + { + data = Convert.FromBase64String(requestContent.Data); + } + catch (FormatException) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); + } + + createContent = new TimelinePostCreateRequestImageContent(data); + break; + default: + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); + } - else if (content.Type == TimelinePostContentTypes.Image) - { - var base64Data = content.Data; - if (base64Data == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); - } - byte[] data; - try - { - data = Convert.FromBase64String(base64Data); - } - catch (FormatException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); - } - try - { - post = await _postService.CreateImagePost(timelineId, userId, data, properties); - } - catch (ImageException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); - } + try + { + var post = await _postService.CreatePost(timelineId, userId, new TimelinePostCreateRequest(createContent) { Time = body.Time, Color = body.Color }); + var result = await _timelineMapper.MapToHttp(post, timeline, Url); + return result; } - else + catch (ImageException) { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); } - - var result = await _timelineMapper.MapToHttp(post, timeline, Url); - return result; } /// diff --git a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs index 22763eba..ca5e79e1 100644 --- a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs +++ b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs @@ -4,7 +4,10 @@ namespace Timeline.Models { public static class TimelinePostContentTypes { +#pragma warning disable CA1819 // Properties should not return arrays public static string[] AllTypes { get; } = new string[] { Text, Image }; +#pragma warning restore CA1819 // Properties should not return arrays + public const string Text = "text"; public const string Image = "image"; } diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index cf5f4e55..076c45e8 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -24,12 +24,50 @@ namespace Timeline.Services public DateTime? LastModified { get; set; } // TODO: Why nullable? } - public class TimelinePostCommonProperties + public abstract class TimelinePostCreateRequestContent { + public abstract string TypeName { get; } + } + + public class TimelinePostCreateRequestTextContent : TimelinePostCreateRequestContent + { + public TimelinePostCreateRequestTextContent(string text) + { + Text = text; + } + + public override string TypeName => TimelinePostContentTypes.Text; + + public string Text { get; set; } + } + + public class TimelinePostCreateRequestImageContent : TimelinePostCreateRequestContent + { + public TimelinePostCreateRequestImageContent(byte[] data) + { + Data = data; + } + + public override string TypeName => TimelinePostContentTypes.Image; + +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } +#pragma warning restore CA1819 // Properties should not return arrays + } + + public class TimelinePostCreateRequest + { + public TimelinePostCreateRequest(TimelinePostCreateRequestContent content) + { + Content = content; + } + public string? Color { get; set; } /// If not set, current time is used. public DateTime? Time { get; set; } + + public TimelinePostCreateRequestContent Content { get; set; } } public interface ITimelinePostService @@ -79,31 +117,18 @@ namespace Timeline.Services Task GetPostData(long timelineId, long postId); /// - /// Create a new text post in timeline. + /// Create a new post in timeline. /// /// The id of the timeline to create post against. /// The author's user id. - /// The content text. - /// Some properties. + /// Info about the post. /// The info of the created post. - /// Thrown when is null. - /// Thrown when timeline does not exist. - /// Thrown if user of does not exist. - Task CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null); - - /// - /// Create a new image post in timeline. - /// - /// The id of the timeline to create post against. - /// The author's user id. - /// The image data. - /// Some properties. - /// The info of the created post. - /// Thrown when is null. + /// Thrown when is null. + /// Thrown when is of invalid format. /// Thrown when timeline does not exist. /// Thrown if user of does not exist. /// Thrown if data is not a image. Validated by . - Task CreateImagePost(long timelineId, long authorId, byte[] imageData, TimelinePostCommonProperties? properties = null); + Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request); /// /// Delete a post. @@ -284,22 +309,27 @@ namespace Timeline.Services }; } - private async Task GeneralCreatePost(long timelineId, long authorId, TimelinePostCommonProperties? properties, Func saveContent) + public async Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request) { - if (properties is not null) + if (request is null) + throw new ArgumentNullException(nameof(request)); + + + if (request.Content is null) + throw new ArgumentException("Content is null.", nameof(request)); + { - if (!_colorValidator.Validate(properties.Color, out var message)) - { - throw new ArgumentException(message, nameof(properties)); - } - properties.Time = properties.Time?.MyToUtc(); + if (!_colorValidator.Validate(request.Color, out var message)) + throw new ArgumentException("Color is not valid.", nameof(request)); } + request.Time = request.Time?.MyToUtc(); + await CheckTimelineExistence(timelineId); await CheckUserExistence(authorId); var currentTime = _clock.GetCurrentTime(); - var finalTime = properties?.Time ?? currentTime; + var finalTime = request.Time ?? currentTime; var postEntity = new TimelinePostEntity { @@ -307,10 +337,29 @@ namespace Timeline.Services TimelineId = timelineId, Time = finalTime, LastUpdated = currentTime, - Color = properties?.Color + Color = request.Color + }; + + switch (request.Content) + { + case TimelinePostCreateRequestTextContent content: + postEntity.ContentType = content.TypeName; + postEntity.Content = content.Text; + break; + case TimelinePostCreateRequestImageContent content: + var imageFormat = await _imageValidator.Validate(content.Data); + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(content.Data); + + postEntity.ContentType = content.TypeName; + postEntity.Content = tag; + postEntity.ExtraContent = imageFormatText; + break; + default: + throw new ArgumentException("Unknown content type.", nameof(request)); }; - await saveContent(postEntity); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); timelineEntity.CurrentPostLocalId += 1; @@ -323,40 +372,6 @@ namespace Timeline.Services return postEntity; } - public async Task CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null) - { - if (text is null) - throw new ArgumentNullException(nameof(text)); - - return await GeneralCreatePost(timelineId, authorId, properties, (entity) => - { - entity.ContentType = TimelinePostContentTypes.Text; - entity.Content = text; - - return Task.CompletedTask; - }); - } - - public async Task CreateImagePost(long timelineId, long authorId, byte[] data, TimelinePostCommonProperties? properties = null) - { - if (data is null) - throw new ArgumentNullException(nameof(data)); - - await CheckTimelineExistence(timelineId); - - return await GeneralCreatePost(timelineId, authorId, properties, async (entity) => - { - var imageFormat = await _imageValidator.Validate(data); - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(data); - - entity.ContentType = TimelinePostContentTypes.Image; - entity.Content = tag; - entity.ExtraContent = imageFormatText; - }); - } - public async Task DeletePost(long timelineId, long postId) { await CheckTimelineExistence(timelineId); -- cgit v1.2.3 From 8b7b570b008c9e9c8c0660c9650112d435269924 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 8 Feb 2021 19:12:44 +0800 Subject: ... --- BackEnd/Timeline/Services/TimelinePostService.cs | 178 ++++++++++++++++++----- 1 file changed, 140 insertions(+), 38 deletions(-) (limited to 'BackEnd/Timeline') diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 076c45e8..66ec8090 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -31,27 +31,55 @@ namespace Timeline.Services public class TimelinePostCreateRequestTextContent : TimelinePostCreateRequestContent { + private string _text; + public TimelinePostCreateRequestTextContent(string text) { - Text = text; + if (text is null) + throw new ArgumentNullException(nameof(text)); + + _text = text; } public override string TypeName => TimelinePostContentTypes.Text; - public string Text { get; set; } + public string Text + { + get => _text; + set + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + _text = value; + } + } } public class TimelinePostCreateRequestImageContent : TimelinePostCreateRequestContent { + private byte[] _data; + public TimelinePostCreateRequestImageContent(byte[] data) { - Data = data; + if (data is null) + throw new ArgumentNullException(nameof(data)); + + _data = data; } public override string TypeName => TimelinePostContentTypes.Image; #pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } + public byte[] Data + { + get => _data; + set + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + _data = value; + } + } #pragma warning restore CA1819 // Properties should not return arrays } @@ -70,6 +98,13 @@ namespace Timeline.Services public TimelinePostCreateRequestContent Content { get; set; } } + public class TimelinePostPatchRequest + { + public string? Color { get; set; } + public DateTime? Time { get; set; } + public TimelinePostCreateRequestContent? Content { get; set; } + } + public interface ITimelinePostService { /// @@ -122,7 +157,7 @@ namespace Timeline.Services /// The id of the timeline to create post against. /// The author's user id. /// Info about the post. - /// The info of the created post. + /// The entity of the created post. /// Thrown when is null. /// Thrown when is of invalid format. /// Thrown when timeline does not exist. @@ -130,6 +165,20 @@ namespace Timeline.Services /// Thrown if data is not a image. Validated by . Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request); + /// + /// Modify a post. Change its properties or replace its content. + /// + /// The timeline id. + /// The post id. + /// The request. + /// The entity of the patched post. + /// Thrown when is null. + /// Thrown when is of invalid format. + /// Thrown when timeline does not exist. + /// Thrown when post does not exist. + /// Thrown if data is not a image. Validated by . + Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request); + /// /// Delete a post. /// @@ -309,6 +358,36 @@ namespace Timeline.Services }; } + private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestContent content) + { + switch (content) + { + case TimelinePostCreateRequestTextContent c: + entity.ContentType = c.TypeName; + entity.Content = c.Text; + break; + case TimelinePostCreateRequestImageContent c: + var imageFormat = await _imageValidator.Validate(c.Data); + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(c.Data); + + entity.ContentType = content.TypeName; + entity.Content = tag; + entity.ExtraContent = imageFormatText; + break; + default: + throw new ArgumentException("Unknown content type.", nameof(content)); + }; + } + + private async Task CleanContent(TimelinePostEntity entity) + { + if (entity.Content is not null && entity.ContentType == TimelinePostContentTypes.Image) + await _dataManager.FreeEntry(entity.Content); + entity.Content = null; + } + public async Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request) { if (request is null) @@ -331,6 +410,8 @@ namespace Timeline.Services var currentTime = _clock.GetCurrentTime(); var finalTime = request.Time ?? currentTime; + await using var transaction = await _database.Database.BeginTransactionAsync(); + var postEntity = new TimelinePostEntity { AuthorId = authorId, @@ -340,26 +421,7 @@ namespace Timeline.Services Color = request.Color }; - switch (request.Content) - { - case TimelinePostCreateRequestTextContent content: - postEntity.ContentType = content.TypeName; - postEntity.Content = content.Text; - break; - case TimelinePostCreateRequestImageContent content: - var imageFormat = await _imageValidator.Validate(content.Data); - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(content.Data); - - postEntity.ContentType = content.TypeName; - postEntity.Content = tag; - postEntity.ExtraContent = imageFormatText; - break; - default: - throw new ArgumentException("Unknown content type.", nameof(request)); - }; - + await SaveContent(postEntity, request.Content); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); timelineEntity.CurrentPostLocalId += 1; @@ -369,37 +431,77 @@ namespace Timeline.Services await _database.SaveChangesAsync(); + await transaction.CommitAsync(); + return postEntity; } - public async Task DeletePost(long timelineId, long postId) + public async Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request) { + if (request is null) + throw new ArgumentNullException(nameof(request)); + + { + if (!_colorValidator.Validate(request.Color, out var message)) + throw new ArgumentException("Color is not valid.", nameof(request)); + } + + request.Time = request.Time?.MyToUtc(); + await CheckTimelineExistence(timelineId); - var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - if (post == null) + await using var transaction = await _database.Database.BeginTransactionAsync(); + + if (entity is null) throw new TimelinePostNotExistException(timelineId, postId, false); - if (post.Content == null) + if (entity.Content is null) throw new TimelinePostNotExistException(timelineId, postId, true); - string? dataTag = null; + if (request.Time.HasValue) + entity.Time = request.Time.Value; + + if (request.Color is not null) + entity.Color = request.Color; - if (post.ContentType == TimelinePostContentTypes.Image) + if (request.Content is not null) { - dataTag = post.Content; + await CleanContent(entity); + await SaveContent(entity, request.Content); } - post.Content = null; - post.LastUpdated = _clock.GetCurrentTime(); + entity.LastUpdated = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); - if (dataTag != null) - { - await _dataManager.FreeEntry(dataTag); - } + await transaction.CommitAsync(); + + return entity; + } + + public async Task DeletePost(long timelineId, long postId) + { + await CheckTimelineExistence(timelineId); + + var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (entity == null) + throw new TimelinePostNotExistException(timelineId, postId, false); + + if (entity.Content == null) + throw new TimelinePostNotExistException(timelineId, postId, true); + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + await CleanContent(entity); + + entity.LastUpdated = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + await transaction.CommitAsync(); } public async Task DeleteAllPostsOfUser(long userId) -- cgit v1.2.3 From 45b683d582d4a7760ffd69c4cd47841ce0545119 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 8 Feb 2021 21:24:42 +0800 Subject: ... --- .../IntegratedTests/TimelinePostTest.cs | 30 +++++++++++++++++++ .../Timeline/Controllers/TimelinePostController.cs | 35 +++++++++++++++++++++- .../Models/Http/HttpTimelinePostPatchRequest.cs | 20 ------------- 3 files changed, 64 insertions(+), 21 deletions(-) (limited to 'BackEnd/Timeline') diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index b4ddcb43..17c85f22 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -535,5 +535,35 @@ namespace Timeline.Tests.IntegratedTests await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/{post.Id}"); } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task PatchPost(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest + { + Content = new() + { + Type = "text", + Text = "aaa" + } + }); + + var date = new DateTime(2000, 10, 1); + + var post2 = await client.TestPatchAsync($"timelines/{generator(1)}/posts/{post.Id}", new HttpTimelinePostPatchRequest + { + Time = date, + Color = "#aabbcc" + }); + post2.Time.Should().Be(date); + post2.Color.Should().Be("#aabbcc"); + + var post3 = await client.TestGetAsync($"timelines/{generator(1)}/posts/{post.Id}"); + post3.Time.Should().Be(date); + post3.Color.Should().Be("#aabbcc"); + } } } diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index 4dab2a44..44498c58 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Threading.Tasks; -using Timeline.Entities; using Timeline.Filters; using Timeline.Helpers; using Timeline.Models; @@ -209,6 +208,40 @@ namespace Timeline.Controllers } } + /// + /// Update a post except content. + /// + /// Timeline name. + /// Post id. + /// Request body. + /// New info of post. + [HttpPatch("{post}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> Patch([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromBody] HttpTimelinePostPatchRequest body) + { + var timelineId = await _timelineService.GetTimelineIdByName(timeline); + + try + { + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + var entity = await _postService.PatchPost(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); + var result = await _timelineMapper.MapToHttp(entity, timeline, Url); + return Ok(result); + } + catch (TimelinePostNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.PostNotExist()); + } + } + /// /// Delete a post. /// diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs index 3dface29..2c6edf66 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs @@ -3,26 +3,6 @@ using Timeline.Models.Validation; namespace Timeline.Models.Http { - /// - /// The model of changing post content. - /// - public class HttpTimelinePostPatchRequestContent - { - /// - /// The new type of the post. If null, old type is used. This field can't be used alone. Use it with corresponding fields to change post content. - /// - [TimelinePostContentType] - public string? Type { get; set; } - /// - /// The new text. Null for not change. - /// - public string? Text { get; set; } - /// - /// The new data. Null for not change. - /// - public string? Data { get; set; } - } - public class HttpTimelinePostPatchRequest { /// -- cgit v1.2.3