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 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) (limited to 'BackEnd/Timeline/Controllers/TimelinePostController.cs') 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); -- 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/Controllers/TimelinePostController.cs') 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 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/Controllers/TimelinePostController.cs') 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