From 97f74b01b99ec7fa68b8ccb12aec77c6f3edc3ac Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 18 Jun 2019 15:19:35 +0200 Subject: [PATCH 01/10] Added missing method to workflow. --- src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index 06738ec71..e2b21d0af 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Contents Task IsValidNextStatus(IContentEntity content, Status2 next); + Task CanUpdateAsync(IContentEntity content); + Task GetNextsAsync(IContentEntity content); Task GetAllAsync(ISchemaEntity schema); From 5002b7041e3cdfc5ecba8329d56680df1e92c2bd Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 18 Jun 2019 15:53:53 +0200 Subject: [PATCH 02/10] Use content workflow interface in controller. --- .../Contents/ContentQueryService.cs | 13 +++---- .../Contents/IContentQueryService.cs | 5 +-- .../Contents/ContentsController.cs | 31 +++++++++------- .../Areas/Api/Controllers/Contents/Helper.cs | 7 ++++ .../Controllers/Contents/Models/ContentDto.cs | 26 ++++++++++---- .../Contents/Models/ContentsDto.cs | 36 ++++++++++++++----- .../Contents/ContentQueryServiceTests.cs | 8 ++--- 7 files changed, 84 insertions(+), 42 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 5d93788fe..b67536690 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -73,16 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents this.scriptEngine = scriptEngine; } - public Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName) - { - return GetSchemaAsync(context, schemaIdOrName); - } - public async Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1) { Guard.NotNull(context, nameof(context)); - var schema = await GetSchemaAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context.User, schema); @@ -110,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { Guard.NotNull(context, nameof(context)); - var schema = await GetSchemaAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context.User, schema); @@ -136,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) + public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) { Guard.NotNull(context, nameof(context)); @@ -295,7 +290,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public async Task GetSchemaAsync(QueryContext context, string schemaIdOrName) + public async Task GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName) { ISchemaEntity schema = null; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 75d89c115..769422de1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents @@ -16,12 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Contents { int DefaultPageSizeGraphQl { get; } - Task> QueryAsync(QueryContext context, IReadOnlyList ids); + Task> QueryAsync(QueryContext context, IReadOnlyList ids); Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query); Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any); - Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName); + Task GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName); } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 675833005..a8a4d1e87 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; @@ -26,15 +27,18 @@ namespace Squidex.Areas.Api.Controllers.Contents { private readonly IOptions controllerOptions; private readonly IContentQueryService contentQuery; + private readonly IContentWorkflow contentWorkflow; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, + IContentWorkflow contentWorkflow, IGraphQLService graphQl, IOptions controllerOptions) : base(commandBus) { this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; this.controllerOptions = controllerOptions; this.graphQl = graphQl; @@ -123,8 +127,9 @@ namespace Squidex.Areas.Api.Controllers.Contents { var context = Context(); var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids); + var contentsList = ResultList.Create(contents.Count, contents); - var response = ContentsDto.FromContents(contents.Count, contents, context, this, app, null); + var response = await ContentsDto.FromContentsAsync(contentsList, context, this, null, contentWorkflow); if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) { @@ -159,7 +164,9 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var contents = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); - var response = ContentsDto.FromContents(contents.Total, contents, context, this, app, name); + var schema = await contentQuery.GetSchemaOrThrowAsync(context, name); + + var response = await ContentsDto.FromContentsAsync(contents, context, this, schema, contentWorkflow); if (ShouldProvideSurrogateKeys(response)) { @@ -194,7 +201,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var content = await contentQuery.FindContentAsync(context, name, id); - var response = ContentDto.FromContent(content, context, this, app, name); + var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -230,7 +237,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var content = await contentQuery.FindContentAsync(context, name, id, version); - var response = ContentDto.FromContent(content, context, this, app, name); + var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -264,7 +271,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); if (publish && !this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) { @@ -301,7 +308,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; @@ -333,7 +340,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; @@ -364,9 +371,9 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); - if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) + if (!this.HasPermission(Helper.StatusPermission(app, name, Status2.Published))) { return new ForbidResult(); } @@ -399,7 +406,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task DiscardDraft(string app, string name, Guid id) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new DiscardChanges { ContentId = id }; @@ -427,7 +434,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task DeleteContent(string app, string name, Guid id) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new DeleteContent { ContentId = id }; @@ -441,7 +448,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = ContentDto.FromContent(result, null, this, app, schema); + var response = await ContentDto.FromContentAsync(null, result, contentWorkflow, this); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs index e65648c21..be1f7ef46 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs @@ -19,5 +19,12 @@ namespace Squidex.Areas.Api.Controllers.Contents return Permissions.ForApp(id, app, schema); } + + public static Permission StatusPermission(string app, string schema, Status2 status) + { + var id = Permissions.AppContentsStatus.Replace("{status}", status.Name); + + return Permissions.ForApp(id, app, schema); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 3939cfd67..c699d7017 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -7,6 +7,7 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; @@ -79,7 +80,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// public long Version { get; set; } - public static ContentDto FromContent(IContentEntity content, QueryContext context, ApiController controller, string app, string schema) + public static ValueTask FromContentAsync( + QueryContext context, + IContentEntity content, + IContentWorkflow contentWorkflow, + ApiController controller) { var response = SimpleMapper.Map(content, new ContentDto()); @@ -99,10 +104,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); } - return response.CreateLinks(controller, app, schema); + return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name, contentWorkflow); } - private ContentDto CreateLinks(ApiController controller, string app, string schema) + private async ValueTask CreateLinksAsync(IContentEntity content, + ApiController controller, + string app, + string schema, + IContentWorkflow contentWorkflow) { var values = new { app, name = schema, id = Id }; @@ -122,7 +131,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); } - if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published))) + if (controller.HasPermission(Helper.StatusPermission(app, schema, Status2.Published))) { AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); } @@ -130,7 +139,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) { - AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); + if (await contentWorkflow.CanUpdateAsync(content)) + { + AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); + } if (Status == Status.Published) { @@ -145,7 +157,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddPutLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); } - foreach (var next in StatusFlow.Next(Status)) + var nextStatuses = await contentWorkflow.GetNextsAsync(content); + + foreach (var next in nextStatuses) { if (controller.HasPermission(Helper.StatusPermission(app, schema, next))) { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 9f664880e..928cc12b2 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -5,12 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Shared; using Squidex.Web; @@ -34,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// The possible statuses. /// [Required] - public string[] Statuses { get; set; } + public Status2[] Statuses { get; set; } public string ToEtag() { @@ -46,20 +47,37 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models return Items.ToSurrogateKeys(); } - public static ContentsDto FromContents(long total, IEnumerable contents, QueryContext context, + public static async Task FromContentsAsync(IResultList contents, QueryContext context, ApiController controller, - string app, - string schema) + ISchemaEntity schema, + IContentWorkflow contentWorkflow) { var result = new ContentsDto { - Total = total, - Items = contents.Select(x => ContentDto.FromContent(x, context, controller, app, schema)).ToArray(), + Total = contents.Total, + Items = new ContentDto[contents.Count] }; - result.Statuses = new string[] { "Archived", "Draft", "Published" }; + await Task.WhenAll( + result.AssignContentsAsync(contentWorkflow, contents, context, controller), + result.AssignStatusesAsync(contentWorkflow, schema)); - return result.CreateLinks(controller, app, schema); + return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); + } + + private async Task AssignStatusesAsync(IContentWorkflow contentWorkflow, ISchemaEntity schema) + { + var allStatuses = await contentWorkflow.GetAllAsync(schema); + + Statuses = allStatuses.ToArray(); + } + + private async Task AssignContentsAsync(IContentWorkflow contentWorkflow, IResultList contents, QueryContext context, ApiController controller) + { + for (var i = 0; i < Items.Length; i++) + { + Items[i] = await ContentDto.FromContentAsync(context, contents[i], contentWorkflow, controller); + } } private ContentsDto CreateLinks(ApiController controller, string app, string schema) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 9ca83efa0..546454b83 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -103,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchemaFound(); - var result = await sut.GetSchemaAsync(context, schemaId.Name); + var result = await sut.GetSchemaOrThrowAsync(context, schemaId.Name); Assert.Equal(schema, result); } @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchemaFound(); - var result = await sut.GetSchemaAsync(context, schemaId.Name); + var result = await sut.GetSchemaOrThrowAsync(context, schemaId.Name); Assert.Equal(schema, result); } @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var ctx = context; - await Assert.ThrowsAsync(() => sut.GetSchemaAsync(ctx, schemaId.Name)); + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); } [Fact] @@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var ctx = context; - await Assert.ThrowsAsync(() => sut.ThrowIfSchemaNotExistsAsync(ctx, schemaId.Name)); + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); } [Fact] From de881c88a311e5e435598b16e30c8e6811c02b20 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 18 Jun 2019 16:14:04 +0200 Subject: [PATCH 03/10] Finalize status. --- .../Contents/Json/StatusConverter.cs | 42 +++++++++++++ .../Contents/Status2.cs | 2 +- .../Config/Domain/SerializationServices.cs | 2 + .../Model/Contents/StatusTests.cs | 60 +++++++++++++++++++ .../TestUtils.cs | 2 + 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs new file mode 100644 index 000000000..6c4266e08 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using System; +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core.Contents.Json +{ + public sealed class StatusConverter : JsonConverter, ISupportedTypes + { + public IEnumerable SupportedTypes + { + get { yield return typeof(Status2); } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.String) + { + throw new JsonException($"Expected String, but got {reader.TokenType}."); + } + + return new Status2(reader.Value.ToString()); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Status2); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs index 3e684bbe7..7ce6e830c 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.Contents public override int GetHashCode() { - return base.GetHashCode(); + return Name.GetHashCode(); } public override string ToString() diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index 0b1f7cfce..44aadc738 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Contents.Json; using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; @@ -44,6 +45,7 @@ namespace Squidex.Config.Domain new RolesConverter(), new RuleConverter(), new SchemaConverter(), + new StatusConverter(), new StringEnumConverter()); settings.NullValueHandling = NullValueHandling.Ignore; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs new file mode 100644 index 000000000..5abdd5f2d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Contents +{ + public class StatusTests + { + [Fact] + public void Should_initialize_status_from_string() + { + var result = new Status2("Draft"); + + Assert.Equal("Draft", result.Name); + Assert.Equal("Draft", result.ToString()); + } + + [Fact] + public void Should_provide_published_status() + { + var result = Status2.Published; + + Assert.Equal("Published", result.Name); + Assert.Equal("Published", result.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var status_1_a = new Status2("Draft"); + var status_1_b = new Status2("Draft"); + + var status2_a = new Status2("Published"); + + Assert.Equal(status_1_a, status_1_b); + Assert.Equal(status_1_a.GetHashCode(), status_1_b.GetHashCode()); + Assert.True(status_1_a.Equals((object)status_1_b)); + + Assert.NotEqual(status_1_a, status2_a); + Assert.NotEqual(status_1_a.GetHashCode(), status2_a.GetHashCode()); + Assert.False(status_1_a.Equals((object)status2_a)); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var status = new Status2("Draft"); + + var serialized = status.SerializeAndDeserialize(); + + Assert.Equal(status, serialized); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs index e3e34b385..58692226c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs @@ -11,6 +11,7 @@ using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Contents.Json; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; @@ -56,6 +57,7 @@ namespace Squidex.Domain.Apps.Core new RolesConverter(), new RuleConverter(), new SchemaConverter(), + new StatusConverter(), new StringEnumConverter()), TypeNameHandling = typeNameHandling From 7ff53516728891eea6d5f370405bcac54532b0c0 Mon Sep 17 00:00:00 2001 From: Ben-Fletcher-UK <49555119+Ben-Fletcher-UK@users.noreply.github.com> Date: Wed, 19 Jun 2019 12:48:40 +0100 Subject: [PATCH 04/10] Mapped StatusFlow to new DefaultContentWorkflow (#365) * Mapped StatusFlow to new DefaultContentWorkflow and Added Status2 to IContentEntity * Added DefaultContentWorkflow to DI and added tests. * Addressed feedback from Code Review. * Removed unused reference. --- .../Contents/DefaultContentWorkflow.cs | 75 +++++++++++++ src/Squidex/Config/Domain/EntitiesServices.cs | 3 + .../Squidex.Domain.Apps.Core.Tests.csproj | 1 + .../Contents/DefaultContentWorkflowTests.cs | 101 ++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs new file mode 100644 index 000000000..a75dd30a2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class DefaultContentWorkflow : IContentWorkflow + { + private static readonly Status2 Draft = new Status2("Draft"); + private static readonly Status2 Archived = new Status2("Archived"); + private static readonly Status2 Published = new Status2("Published"); + + private static readonly Dictionary Flow = new Dictionary + { + [Draft] = new[] { Published, Archived }, + [Archived] = new[] { Draft }, + [Published] = new[] { Draft, Archived } + }; + + public Task GetInitialStatusAsync(ISchemaEntity schema) + { + return Task.FromResult(Draft); + } + + public Task IsValidNextStatus(IContentEntity content, Status2 next) + { + return TaskHelper.True; + } + + public Task CanUpdateAsync(IContentEntity content) + { + return TaskHelper.True; + } + + public Task GetNextsAsync(IContentEntity content) + { + Status2 statusToCheck; + + switch (content.Status) + { + case Status.Draft: + statusToCheck = Draft; + break; + case Status.Archived: + statusToCheck = Archived; + break; + case Status.Published: + statusToCheck = Published; + break; + default: + { + statusToCheck = Draft; + break; + } + } + + return Task.FromResult(Flow.TryGetValue(statusToCheck, out var result) ? result : Array.Empty()); + } + + public Task GetAllAsync(ISchemaEntity schema) + { + return Task.FromResult(new[] { Draft, Archived, Published } ); + } + } +} diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 1b378c173..d44e12150 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -101,6 +101,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .AsOptional(); + services.AddSingletonAs() .AsSelf(); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 7c5aefb48..0b4ac139b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs new file mode 100644 index 000000000..f9f368f6f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DefaultContentWorkflowTests + { + private static readonly DefaultContentWorkflow Sut = new DefaultContentWorkflow(); + + [Fact] + public async Task Should_draft_as_initial_status_async_tests() + { + var result = await Sut.GetInitialStatusAsync(null); + + Assert.IsType(result); + Assert.Equal("Draft", result.Name); + } + + [Fact] + public async Task Should_check_is_valid_next_status_tests() + { + var entity = CreateMockContentEntity(Status.Draft); + + var status = new Status2("Draft"); + + var result = await Sut.IsValidNextStatus(entity, status); + + Assert.IsType(result); + Assert.True(result); + } + + [Fact] + public async Task Should_update_async_tests() + { + var entity = CreateMockContentEntity(Status.Draft); + + var result = await Sut.CanUpdateAsync(entity); + + Assert.IsType(result); + Assert.True(result); + } + + [Fact] + public async Task Should_get_nexts_async_tests() + { + var draftContent = CreateMockContentEntity(Status.Draft); + var archivedContent = CreateMockContentEntity(Status.Archived); + var publishedContent = CreateMockContentEntity(Status.Published); + + var draftExpected = new[] { new Status2("Published"), new Status2("Archived") }; + var archivedExpected = new[] { new Status2("Draft") }; + var publishedExpected = new[] { new Status2("Draft"), new Status2("Archived") }; + + var draftResult = await Sut.GetNextsAsync(draftContent); + var archivedResult = await Sut.GetNextsAsync(archivedContent); + var publishedResult = await Sut.GetNextsAsync(publishedContent); + + Assert.IsType(draftResult); + Assert.IsType(archivedResult); + Assert.IsType(publishedResult); + + Assert.Equal(draftExpected, draftResult); + Assert.Equal(archivedExpected, archivedResult); + Assert.Equal(publishedExpected, publishedResult); + } + + [Fact] + public async Task Should_get_all_async_tests() + { + var expected = new[] { new Status2("Draft"), new Status2("Archived"), new Status2("Published") }; + + var result = await Sut.GetAllAsync(null); + + Assert.IsType(result); + Assert.Equal(expected, result); + } + + private IContentEntity CreateMockContentEntity(Status status) + { + var content = A.Fake(); + + A.CallTo(() => content.Id).Returns(default(Guid)); + A.CallTo(() => content.Data).Returns(null); + A.CallTo(() => content.DataDraft).Returns(null); + A.CallTo(() => content.SchemaId).Returns(null); + A.CallTo(() => content.Status).Returns(status); + + return content; + } + } +} From 20787820578d796ce295e4c57a2657b10b126c29 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 19 Jun 2019 14:14:33 +0200 Subject: [PATCH 05/10] Formatting. --- .../Contents/DefaultContentWorkflowTests.cs | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index f9f368f6f..25d9d8bb8 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Contents; @@ -15,73 +14,79 @@ namespace Squidex.Domain.Apps.Entities.Contents { public class DefaultContentWorkflowTests { - private static readonly DefaultContentWorkflow Sut = new DefaultContentWorkflow(); + private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); [Fact] - public async Task Should_draft_as_initial_status_async_tests() + public async Task Should_draft_as_initial_status() { - var result = await Sut.GetInitialStatusAsync(null); + var result = await sut.GetInitialStatusAsync(null); - Assert.IsType(result); - Assert.Equal("Draft", result.Name); + Assert.Equal(new Status2("Draft"), result); } [Fact] - public async Task Should_check_is_valid_next_status_tests() + public async Task Should_check_is_valid_next() { var entity = CreateMockContentEntity(Status.Draft); - var status = new Status2("Draft"); + var result = await sut.IsValidNextStatus(entity, new Status2("Draft")); - var result = await Sut.IsValidNextStatus(entity, status); - - Assert.IsType(result); Assert.True(result); } [Fact] - public async Task Should_update_async_tests() + public async Task Should_always_be_able_to_update() { var entity = CreateMockContentEntity(Status.Draft); - var result = await Sut.CanUpdateAsync(entity); + var result = await sut.CanUpdateAsync(entity); - Assert.IsType(result); Assert.True(result); } [Fact] - public async Task Should_get_nexts_async_tests() + public async Task Should_get_next_statuses_for_draft() { - var draftContent = CreateMockContentEntity(Status.Draft); - var archivedContent = CreateMockContentEntity(Status.Archived); - var publishedContent = CreateMockContentEntity(Status.Published); + var content = CreateMockContentEntity(Status.Draft); - var draftExpected = new[] { new Status2("Published"), new Status2("Archived") }; - var archivedExpected = new[] { new Status2("Draft") }; - var publishedExpected = new[] { new Status2("Draft"), new Status2("Archived") }; + var expected = new[] { new Status2("Published"), new Status2("Archived") }; - var draftResult = await Sut.GetNextsAsync(draftContent); - var archivedResult = await Sut.GetNextsAsync(archivedContent); - var publishedResult = await Sut.GetNextsAsync(publishedContent); + var result = await sut.GetNextsAsync(content); - Assert.IsType(draftResult); - Assert.IsType(archivedResult); - Assert.IsType(publishedResult); + Assert.Equal(expected, result); + } - Assert.Equal(draftExpected, draftResult); - Assert.Equal(archivedExpected, archivedResult); - Assert.Equal(publishedExpected, publishedResult); + [Fact] + public async Task Should_get_next_statuses_for_archived() + { + var content = CreateMockContentEntity(Status.Archived); + + var expected = new[] { new Status2("Draft") }; + + var result = await sut.GetNextsAsync(content); + + Assert.Equal(expected, result); + } + + [Fact] + public async Task Should_get_next_statuses_for_published() + { + var content = CreateMockContentEntity(Status.Published); + + var expected = new[] { new Status2("Draft"), new Status2("Archived") }; + + var result = await sut.GetNextsAsync(content); + + Assert.Equal(expected, result); } [Fact] - public async Task Should_get_all_async_tests() + public async Task Should_return_all_statuses() { var expected = new[] { new Status2("Draft"), new Status2("Archived"), new Status2("Published") }; - var result = await Sut.GetAllAsync(null); + var result = await sut.GetAllAsync(null); - Assert.IsType(result); Assert.Equal(expected, result); } @@ -89,10 +94,6 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = A.Fake(); - A.CallTo(() => content.Id).Returns(default(Guid)); - A.CallTo(() => content.Data).Returns(null); - A.CallTo(() => content.DataDraft).Returns(null); - A.CallTo(() => content.SchemaId).Returns(null); A.CallTo(() => content.Status).Returns(status); return content; From 9fed02581b901b030eee7c94fa77166aa3e70259 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 19 Jun 2019 15:55:19 +0200 Subject: [PATCH 06/10] All tests green now. --- .../Contents/Json/StatusConverter.cs | 6 +- .../Contents/Status.cs | 53 +++++++- .../Contents/Status2.cs | 46 ------- .../Contents/StatusChange.cs | 17 --- .../Contents/StatusFlow.cs | 37 ------ .../EnrichedContentEventType.cs | 5 +- .../Contents/MongoContentEntity.cs | 1 - .../Contents/StatusSerializer.cs | 8 +- .../Contents/ContentChangedTriggerHandler.cs | 20 +-- .../Contents/ContentEntity.cs | 21 --- .../Contents/ContentGrain.cs | 59 ++++----- .../Contents/DefaultContentWorkflow.cs | 49 ++----- .../Contents/GraphQL/Types/AllTypes.cs | 4 - .../GraphQL/Types/ContentGraphType.cs | 4 +- .../Contents/Guards/GuardContent.cs | 35 +++-- .../Contents/IContentWorkflow.cs | 8 +- .../Contents/State/ContentState.cs | 5 + .../Contents/ContentCreated.cs | 2 + .../Contents/ContentStatusChanged.cs | 2 - .../Contents/ContentsController.cs | 2 +- .../Areas/Api/Controllers/Contents/Helper.cs | 7 - .../Controllers/Contents/Models/ContentDto.cs | 4 +- .../Contents/Models/ContentsDto.cs | 2 +- .../Model/Contents/StatusFlowTests.cs | 34 ----- .../Model/Contents/StatusTests.cs | 36 +++++- .../ContentChangedTriggerHandlerTests.cs | 6 +- .../Contents/ContentGrainTests.cs | 43 ++++++- .../Contents/ContentQueryServiceTests.cs | 7 +- .../Contents/DefaultContentWorkflowTests.cs | 24 ++-- .../Contents/GraphQL/GraphQLTestBase.cs | 3 +- .../Contents/Guard/GuardContentTests.cs | 120 +++++++++++++----- .../Contents/MongoDb/StatusSerializerTests.cs | 4 +- 32 files changed, 313 insertions(+), 361 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs index 6c4266e08..a56722c55 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json { public IEnumerable SupportedTypes { - get { yield return typeof(Status2); } + get { yield return typeof(Status); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) @@ -31,12 +31,12 @@ namespace Squidex.Domain.Apps.Core.Contents.Json throw new JsonException($"Expected String, but got {reader.TokenType}."); } - return new Status2(reader.Value.ToString()); + return new Status(reader.Value.ToString()); } public override bool CanConvert(Type objectType) { - return objectType == typeof(Status2); + return objectType == typeof(Status); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs index c20e0c4eb..d4c0374c8 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -5,12 +5,57 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure; +using System; + namespace Squidex.Domain.Apps.Core.Contents { - public enum Status + public struct Status : IEquatable { - Draft, - Archived, - Published + public static readonly Status Archived = new Status("Archived"); + public static readonly Status Draft = new Status("Draft"); + public static readonly Status Published = new Status("Published"); + + private readonly string name; + + public string Name + { + get { return name ?? "Unknown"; } + } + + public Status(string name) + { + this.name = name; + } + + public override bool Equals(object obj) + { + return obj is Status status && Equals(status); + } + + public bool Equals(Status other) + { + return string.Equals(name, other.name); + } + + public override int GetHashCode() + { + return name?.GetHashCode() ?? 0; + } + + public override string ToString() + { + return name; + } + + public static bool operator ==(Status lhs, Status rhs) + { + return lhs.Equals(rhs); + } + + public static bool operator !=(Status lhs, Status rhs) + { + return !lhs.Equals(rhs); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs deleted file mode 100644 index 7ce6e830c..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using System; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public struct Status2 : IEquatable - { - public static readonly Status2 Published = new Status2("Published"); - - public string Name { get; } - - public Status2(string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - Name = name; - } - - public override bool Equals(object obj) - { - return obj is Status2 status && Equals(status); - } - - public bool Equals(Status2 other) - { - return Name.Equals(other.Name); - } - - public override int GetHashCode() - { - return Name.GetHashCode(); - } - - public override string ToString() - { - return Name; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs deleted file mode 100644 index 9e3900deb..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Contents -{ - public enum StatusChange - { - Archived, - Published, - Restored, - Unpublished - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs deleted file mode 100644 index 7add93c24..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public static class StatusFlow - { - private static readonly Dictionary Flow = new Dictionary - { - [Status.Draft] = new[] { Status.Published, Status.Archived }, - [Status.Archived] = new[] { Status.Draft }, - [Status.Published] = new[] { Status.Draft, Status.Archived } - }; - - public static bool Exists(Status status) - { - return Flow.ContainsKey(status); - } - - public static bool CanChange(Status status, Status toStatus) - { - return Flow.TryGetValue(status, out var state) && state.Contains(toStatus); - } - - public static IEnumerable Next(Status status) - { - return Flow.TryGetValue(status, out var result) ? result : Enumerable.Empty(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs index 45148a8e2..7ad49332d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs @@ -9,12 +9,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { public enum EnrichedContentEventType { - Archived, Created, Deleted, - Published, - Restored, - Unpublished, + StatusChanged, Updated } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 9c8f3eba7..fe2e0649c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -51,7 +51,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonRequired] [BsonElement("ss")] - [BsonRepresentation(BsonType.String)] public Status Status { get; set; } [BsonIgnoreIfNull] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs index 0b9bf91f0..5d59c836a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs @@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public sealed class StatusSerializer : SerializerBase + public sealed class StatusSerializer : SerializerBase { private static volatile int isRegistered; @@ -24,14 +24,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public override Status2 Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + public override Status Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { var value = context.Reader.ReadString(); - return new Status2(value); + return new Status(value); } - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status2 value) + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status value) { context.Writer.WriteString(value.Name); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index dff657548..0a155f703 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -8,7 +8,6 @@ using System; using System.Threading.Tasks; using Orleans; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -60,23 +59,8 @@ namespace Squidex.Domain.Apps.Entities.Contents case ContentUpdated _: result.Type = EnrichedContentEventType.Updated; break; - case ContentStatusChanged contentStatusChanged: - switch (contentStatusChanged.Change) - { - case StatusChange.Published: - result.Type = EnrichedContentEventType.Published; - break; - case StatusChange.Unpublished: - result.Type = EnrichedContentEventType.Unpublished; - break; - case StatusChange.Archived: - result.Type = EnrichedContentEventType.Archived; - break; - case StatusChange.Restored: - result.Type = EnrichedContentEventType.Restored; - break; - } - + case ContentStatusChanged _: + result.Type = EnrichedContentEventType.StatusChanged; break; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 9c5a5f3f7..8b1b6aac1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -8,9 +8,7 @@ using System; using NodaTime; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Contents { @@ -41,24 +39,5 @@ namespace Squidex.Domain.Apps.Entities.Contents public Status Status { get; set; } public bool IsPending { get; set; } - - public static ContentEntity Create(CreateContent command, EntityCreatedResult result) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - var response = new ContentEntity - { - Id = command.ContentId, - Data = result.IdOrValue, - Version = result.Version, - Created = now, - CreatedBy = command.Actor, - LastModified = now, - LastModifiedBy = command.Actor, - Status = command.Publish ? Status.Published : Status.Draft - }; - - return response; - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index baae5d2dd..0f73ab461 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -32,6 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly IAssetRepository assetRepository; private readonly IContentRepository contentRepository; private readonly IScriptEngine scriptEngine; + private readonly IContentWorkflow contentWorkflow; public ContentGrain( IStore store, @@ -39,17 +40,20 @@ namespace Squidex.Domain.Apps.Entities.Contents IAppProvider appProvider, IAssetRepository assetRepository, IScriptEngine scriptEngine, + IContentWorkflow contentWorkflow, IContentRepository contentRepository) : base(store, log) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); Guard.NotNull(contentRepository, nameof(contentRepository)); this.appProvider = appProvider; this.scriptEngine = scriptEngine; this.assetRepository = assetRepository; + this.contentWorkflow = contentWorkflow; this.contentRepository = contentRepository; } @@ -79,25 +83,27 @@ namespace Squidex.Domain.Apps.Entities.Contents await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data); } - Create(c); + var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema); + + Create(c, status); return Snapshot; }); case UpdateContent updateContent: - return UpdateReturnAsync(updateContent, c => + return UpdateReturnAsync(updateContent, async c => { - GuardContent.CanUpdate(c); + await GuardContent.CanUpdate(Snapshot, contentWorkflow, c); - return UpdateAsync(c, x => c.Data, false); + return await UpdateAsync(c, x => c.Data, false); }); case PatchContent patchContent: - return UpdateReturnAsync(patchContent, c => + return UpdateReturnAsync(patchContent, async c => { - GuardContent.CanPatch(c); + await GuardContent.CanPatch(Snapshot, contentWorkflow, c); - return UpdateAsync(c, c.Data.MergeInto, true); + return await UpdateAsync(c, c.Data.MergeInto, true); }); case ChangeContentStatus changeContentStatus: @@ -107,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to change content."); - GuardContent.CanChangeContentStatus(ctx.Schema, Snapshot.IsPending, Snapshot.Status, c); + await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c); if (c.DueTime.HasValue) { @@ -121,28 +127,11 @@ namespace Squidex.Domain.Apps.Entities.Contents } else { - StatusChange reason; - - if (c.Status == Status.Published) - { - reason = StatusChange.Published; - } - else if (c.Status == Status.Archived) - { - reason = StatusChange.Archived; - } - else if (Snapshot.Status == Status.Published) - { - reason = StatusChange.Unpublished; - } - else - { - reason = StatusChange.Restored; - } - - await ctx.ExecuteScriptAsync(s => s.Change, reason, c, Snapshot.Data); - - ChangeStatus(c, reason); + var operation = c.Status == Status.Published ? "Published" : "StatusChanged"; + + await ctx.ExecuteScriptAsync(s => s.Change, operation, c, Snapshot.Data); + + ChangeStatus(c); } } } @@ -227,13 +216,13 @@ namespace Squidex.Domain.Apps.Entities.Contents return Snapshot; } - public void Create(CreateContent command) + public void Create(CreateContent command, Status status) { - RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); + RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); if (command.Publish) { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })); + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); } } @@ -272,9 +261,9 @@ namespace Squidex.Domain.Apps.Entities.Contents RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); } - public void ChangeStatus(ChangeContentStatus command, StatusChange reason) + public void ChangeStatus(ChangeContentStatus command) { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = reason })); + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); } private void RaiseEvent(SchemaEvent @event) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index a75dd30a2..b1928480a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; @@ -16,25 +17,23 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class DefaultContentWorkflow : IContentWorkflow { - private static readonly Status2 Draft = new Status2("Draft"); - private static readonly Status2 Archived = new Status2("Archived"); - private static readonly Status2 Published = new Status2("Published"); + private static readonly Status[] All = { Status.Archived, Status.Draft, Status.Published }; - private static readonly Dictionary Flow = new Dictionary + private static readonly Dictionary Flow = new Dictionary { - [Draft] = new[] { Published, Archived }, - [Archived] = new[] { Draft }, - [Published] = new[] { Draft, Archived } + [Status.Draft] = new[] { Status.Archived, Status.Published }, + [Status.Archived] = new[] { Status.Draft }, + [Status.Published] = new[] { Status.Draft, Status.Archived } }; - public Task GetInitialStatusAsync(ISchemaEntity schema) + public Task GetInitialStatusAsync(ISchemaEntity schema) { - return Task.FromResult(Draft); + return Task.FromResult(Status.Draft); } - public Task IsValidNextStatus(IContentEntity content, Status2 next) + public Task IsValidNextStatus(IContentEntity content, Status next) { - return TaskHelper.True; + return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next)); } public Task CanUpdateAsync(IContentEntity content) @@ -42,34 +41,14 @@ namespace Squidex.Domain.Apps.Entities.Contents return TaskHelper.True; } - public Task GetNextsAsync(IContentEntity content) + public Task GetNextsAsync(IContentEntity content) { - Status2 statusToCheck; - - switch (content.Status) - { - case Status.Draft: - statusToCheck = Draft; - break; - case Status.Archived: - statusToCheck = Archived; - break; - case Status.Published: - statusToCheck = Published; - break; - default: - { - statusToCheck = Draft; - break; - } - } - - return Task.FromResult(Flow.TryGetValue(statusToCheck, out var result) ? result : Array.Empty()); + return Task.FromResult(Flow.TryGetValue(content.Status, out var result) ? result : Array.Empty()); } - public Task GetAllAsync(ISchemaEntity schema) + public Task GetAllAsync(ISchemaEntity schema) { - return Task.FromResult(new[] { Draft, Archived, Published } ); + return Task.FromResult(All); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs index 5e1b6c973..8f3207ebf 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs @@ -28,8 +28,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IGraphType Float = new FloatGraphType(); - public static readonly IGraphType Status = new EnumerationGraphType(); - public static readonly IGraphType String = new StringGraphType(); public static readonly IGraphType Boolean = new BooleanGraphType(); @@ -46,8 +44,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean); - public static readonly IGraphType NonNullStatusType = new NonNullGraphType(Status); - public static readonly IGraphType NoopDate = new NoopGraphType(Date); public static readonly IGraphType NoopJson = new NoopGraphType(Json); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index ec8878874..a6d876742 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -73,8 +73,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "status", - ResolvedType = AllTypes.NonNullStatusType, - Resolver = Resolve(x => x.Status), + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), Description = $"The the status of the {schemaName} content." }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 379bd601f..35a3acca7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; @@ -30,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards } } - public static void CanUpdate(UpdateContent command) + public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command) { Guard.NotNull(command, nameof(command)); @@ -38,9 +39,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { ValidateData(command, e); }); + + await ValidateCanUpdate(content, contentWorkflow); } - public static void CanPatch(PatchContent command) + public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command) { Guard.NotNull(command, nameof(command)); @@ -48,6 +51,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { ValidateData(command, e); }); + + await ValidateCanUpdate(content, contentWorkflow); } public static void CanDiscardChanges(bool isPending, DiscardChanges command) @@ -60,33 +65,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards } } - public static void CanChangeContentStatus(ISchemaEntity schema, bool isPending, Status status, ChangeContentStatus command) + public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command) { Guard.NotNull(command, nameof(command)); if (schema.SchemaDef.IsSingleton && command.Status != Status.Published) { - throw new DomainException("Singleton content archived or unpublished."); + throw new DomainException("Singleton content cannot be changed."); } - Validate.It(() => "Cannot change status.", e => + return Validate.It(() => "Cannot change status.", async e => { - if (!StatusFlow.Exists(command.Status)) + if (!await contentWorkflow.IsValidNextStatus(content, command.Status)) { - e(Not.Valid("Status"), nameof(command.Status)); - } - else if (!StatusFlow.CanChange(status, command.Status)) - { - if (status == command.Status && status == Status.Published) + if (content.Status == command.Status && content.Status == Status.Published) { - if (!isPending) + if (!content.IsPending) { e("Content has no changes to publish.", nameof(command.Status)); } } else { - e($"Cannot change status from {status} to {command.Status}.", nameof(command.Status)); + e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); } } @@ -114,5 +115,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards e(Not.Defined("Data"), nameof(command.Data)); } } + + private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow) + { + if (!await contentWorkflow.CanUpdateAsync(content)) + { + throw new DomainException($"The workflow does not allow updates at status {content.Status}"); + } + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index e2b21d0af..06de365be 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -13,14 +13,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentWorkflow { - Task GetInitialStatusAsync(ISchemaEntity schema); + Task GetInitialStatusAsync(ISchemaEntity schema); - Task IsValidNextStatus(IContentEntity content, Status2 next); + Task IsValidNextStatus(IContentEntity content, Status next); Task CanUpdateAsync(IContentEntity content); - Task GetNextsAsync(IContentEntity content); + Task GetNextsAsync(IContentEntity content); - Task GetAllAsync(ISchemaEntity schema); + Task GetAllAsync(ISchemaEntity schema); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 1f2169431..0f0234dc9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -50,6 +50,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.State SimpleMapper.Map(@event, this); UpdateData(null, @event.Data, false); + + if (Status == default) + { + Status = Status.Draft; + } } protected void On(ContentChangesPublished @event) diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs index ed06a71ec..317b3b176 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Events.Contents [EventType(nameof(ContentCreated))] public sealed class ContentCreated : ContentEvent { + public Status Status { get; set; } + public NamedContentData Data { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs index 81b95e5fb..2c97f5902 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs @@ -13,8 +13,6 @@ namespace Squidex.Domain.Apps.Events.Contents [EventType(nameof(ContentStatusChanged))] public sealed class ContentStatusChanged : ContentEvent { - public StatusChange? Change { get; set; } - public Status Status { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index a8a4d1e87..833c67dbe 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -373,7 +373,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { await contentQuery.GetSchemaOrThrowAsync(Context(), name); - if (!this.HasPermission(Helper.StatusPermission(app, name, Status2.Published))) + if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) { return new ForbidResult(); } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs index be1f7ef46..8644c925a 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs @@ -14,13 +14,6 @@ namespace Squidex.Areas.Api.Controllers.Contents public static class Helper { public static Permission StatusPermission(string app, string schema, Status status) - { - var id = Permissions.AppContentsStatus.Replace("{status}", status.ToString()); - - return Permissions.ForApp(id, app, schema); - } - - public static Permission StatusPermission(string app, string schema, Status2 status) { var id = Permissions.AppContentsStatus.Replace("{status}", status.Name); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index c699d7017..c83638142 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -131,7 +131,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); } - if (controller.HasPermission(Helper.StatusPermission(app, schema, Status2.Published))) + if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published))) { AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); } @@ -146,7 +146,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models if (Status == Status.Published) { - AddPutLink("draft/propose", controller.Url(x => nameof(x.PutContent), values) + "?asDraft=true"); + AddPutLink("draft/propose", controller.Url((ContentsController x) => nameof(x.PutContent), values) + "?asDraft=true"); } AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 928cc12b2..afcecb7fe 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// The possible statuses. /// [Required] - public Status2[] Statuses { get; set; } + public Status[] Statuses { get; set; } public string ToEtag() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs deleted file mode 100644 index 30126894c..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Contents -{ - public class StatusFlowTests - { - [Fact] - public void Should_make_tests() - { - Assert.True(StatusFlow.Exists(Status.Draft)); - Assert.True(StatusFlow.Exists(Status.Archived)); - Assert.True(StatusFlow.Exists(Status.Published)); - - Assert.True(StatusFlow.CanChange(Status.Draft, Status.Archived)); - Assert.True(StatusFlow.CanChange(Status.Draft, Status.Published)); - - Assert.True(StatusFlow.CanChange(Status.Published, Status.Draft)); - Assert.True(StatusFlow.CanChange(Status.Published, Status.Archived)); - - Assert.True(StatusFlow.CanChange(Status.Archived, Status.Draft)); - - Assert.False(StatusFlow.Exists((Status)int.MaxValue)); - Assert.False(StatusFlow.CanChange(Status.Archived, Status.Published)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs index 5abdd5f2d..138e399c1 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs @@ -15,16 +15,34 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_initialize_status_from_string() { - var result = new Status2("Draft"); + var result = new Status("Custom"); + + Assert.Equal("Custom", result.Name); + Assert.Equal("Custom", result.ToString()); + } + + [Fact] + public void Should_provide_draft_status() + { + var result = Status.Draft; Assert.Equal("Draft", result.Name); Assert.Equal("Draft", result.ToString()); } + [Fact] + public void Should_provide_archived_status() + { + var result = Status.Archived; + + Assert.Equal("Archived", result.Name); + Assert.Equal("Archived", result.ToString()); + } + [Fact] public void Should_provide_published_status() { - var result = Status2.Published; + var result = Status.Published; Assert.Equal("Published", result.Name); Assert.Equal("Published", result.ToString()); @@ -33,10 +51,10 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_make_correct_equal_comparisons() { - var status_1_a = new Status2("Draft"); - var status_1_b = new Status2("Draft"); + var status_1_a = Status.Draft; + var status_1_b = Status.Draft; - var status2_a = new Status2("Published"); + var status2_a = Status.Published; Assert.Equal(status_1_a, status_1_b); Assert.Equal(status_1_a.GetHashCode(), status_1_b.GetHashCode()); @@ -45,12 +63,18 @@ namespace Squidex.Domain.Apps.Core.Model.Contents Assert.NotEqual(status_1_a, status2_a); Assert.NotEqual(status_1_a.GetHashCode(), status2_a.GetHashCode()); Assert.False(status_1_a.Equals((object)status2_a)); + + Assert.True(status_1_a == status_1_b); + Assert.True(status_1_a != status2_a); + + Assert.False(status_1_a != status_1_b); + Assert.False(status_1_a == status2_a); } [Fact] public void Should_serialize_and_deserialize() { - var status = new Status2("Draft"); + var status = Status.Draft; var serialized = status.SerializeAndDeserialize(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 03b547993..1e49dc457 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -11,7 +11,6 @@ using System.Collections.ObjectModel; using System.Threading.Tasks; using FakeItEasy; using Orleans; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -54,10 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents new object[] { new ContentCreated(), EnrichedContentEventType.Created }, new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }, new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }, - new object[] { new ContentStatusChanged { Change = StatusChange.Archived }, EnrichedContentEventType.Archived }, - new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }, - new object[] { new ContentStatusChanged { Change = StatusChange.Restored }, EnrichedContentEventType.Restored }, - new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished } + new object[] { new ContentStatusChanged(), EnrichedContentEventType.StatusChanged } }; [Theory] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs index f5a96de1b..8edba81dd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs @@ -33,6 +33,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { private readonly ISchemaEntity schema = A.Fake(); private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IContentRepository contentRepository = A.Dummy(); + private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly IAppEntity app = A.Fake(); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); @@ -100,9 +102,15 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) .ReturnsLazily(x => x.GetArgument(0).Data); + A.CallTo(() => contentWorkflow.CanUpdateAsync(A.Ignored)) + .Returns(true); + + A.CallTo(() => contentWorkflow.IsValidNextStatus(A.Ignored, A.Ignored)) + .Returns(true); + patched = patch.MergeInto(data); - sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, A.Dummy()); + sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository); sut.ActivateAsync(Id).Wait(); } @@ -135,6 +143,26 @@ namespace Squidex.Domain.Apps.Entities.Contents .MustNotHaveHappened(); } + [Fact] + public async Task Create_should_create_events_and_update_state_with_custom_initial_status() + { + var command = new CreateContent { Data = data }; + + A.CallTo(() => contentWorkflow.GetInitialStatusAsync(schema)) + .Returns(Status.Archived); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Archived, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Archived }) + ); + } + [Fact] public async Task Create_should_also_publish() { @@ -147,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( CreateContentEvent(new ContentCreated { Data = data }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) ); A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) @@ -315,7 +343,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -337,7 +365,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived, Change = StatusChange.Archived }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -360,7 +388,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -383,7 +411,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Restored }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -448,6 +476,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new ChangeContentStatus { Status = Status.Draft, JobId = sut.Snapshot.ScheduleJob.Id }; + A.CallTo(() => contentWorkflow.IsValidNextStatus(sut.Snapshot, command.Status)) + .Returns(false); + var result = await sut.ExecuteAsync(CreateContentCommand(command)); result.ShouldBeEquivalent(sut.Snapshot); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 546454b83..e6fb4334b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -520,7 +520,12 @@ namespace Squidex.Domain.Apps.Entities.Contents .Returns((ISchemaEntity)null); } - private IContentEntity CreateContent(Guid id, Status status = Status.Published) + private IContentEntity CreateContent(Guid id) + { + return CreateContent(id, Status.Published); + } + + private IContentEntity CreateContent(Guid id, Status status) { var content = A.Fake(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index 25d9d8bb8..6a1b5f32a 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -21,15 +21,15 @@ namespace Squidex.Domain.Apps.Entities.Contents { var result = await sut.GetInitialStatusAsync(null); - Assert.Equal(new Status2("Draft"), result); + Assert.Equal(Status.Draft, result); } [Fact] public async Task Should_check_is_valid_next() { - var entity = CreateMockContentEntity(Status.Draft); + var entity = CreateContent(Status.Published); - var result = await sut.IsValidNextStatus(entity, new Status2("Draft")); + var result = await sut.IsValidNextStatus(entity, Status.Draft); Assert.True(result); } @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_always_be_able_to_update() { - var entity = CreateMockContentEntity(Status.Draft); + var entity = CreateContent(Status.Published); var result = await sut.CanUpdateAsync(entity); @@ -47,9 +47,9 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_get_next_statuses_for_draft() { - var content = CreateMockContentEntity(Status.Draft); + var content = CreateContent(Status.Draft); - var expected = new[] { new Status2("Published"), new Status2("Archived") }; + var expected = new[] { Status.Archived, Status.Published }; var result = await sut.GetNextsAsync(content); @@ -59,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_get_next_statuses_for_archived() { - var content = CreateMockContentEntity(Status.Archived); + var content = CreateContent(Status.Archived); - var expected = new[] { new Status2("Draft") }; + var expected = new[] { Status.Draft }; var result = await sut.GetNextsAsync(content); @@ -71,9 +71,9 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_get_next_statuses_for_published() { - var content = CreateMockContentEntity(Status.Published); + var content = CreateContent(Status.Published); - var expected = new[] { new Status2("Draft"), new Status2("Archived") }; + var expected = new[] { Status.Draft, Status.Archived }; var result = await sut.GetNextsAsync(content); @@ -83,14 +83,14 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_all_statuses() { - var expected = new[] { new Status2("Draft"), new Status2("Archived"), new Status2("Published") }; + var expected = new[] { Status.Archived, Status.Draft, Status.Published }; var result = await sut.GetAllAsync(null); Assert.Equal(expected, result); } - private IContentEntity CreateMockContentEntity(Status status) + private IContentEntity CreateContent(Status status) { var content = A.Fake(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index d3f6b5ea1..cfc9e817b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -159,7 +159,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL LastModified = now, LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), Data = data, - DataDraft = dataDraft + DataDraft = dataDraft, + Status = Status.Draft }; return content; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs index 5dda8794d..1f1eb91cb 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; using FakeItEasy; using NodaTime; using Squidex.Domain.Apps.Core.Contents; @@ -21,6 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard public class GuardContentTests { private readonly ISchemaEntity schema = A.Fake(); + private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); [Fact] @@ -65,119 +67,155 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard } [Fact] - public void CanUpdate_should_throw_exception_if_data_is_null() + public async Task CanUpdate_should_throw_exception_if_data_is_null() { SetupSingleton(false); + SetupCanUpdate(true); + var content = CreateContent(Status.Draft, false); var command = new UpdateContent(); - ValidationAssert.Throws(() => GuardContent.CanUpdate(command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command), new ValidationError("Data is required.", "Data")); } [Fact] - public void CanUpdate_should_not_throw_exception_if_data_is_not_null() + public async Task CanUpdate_should_throw_exception_if_workflow_blocks_it() { SetupSingleton(false); + SetupCanUpdate(false); + var content = CreateContent(Status.Draft, false); var command = new UpdateContent { Data = new NamedContentData() }; - GuardContent.CanUpdate(command); + await Assert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command)); } [Fact] - public void CanPatch_should_throw_exception_if_data_is_null() + public async Task CanUpdate_should_not_throw_exception_if_data_is_not_null() { SetupSingleton(false); + SetupCanUpdate(true); + var content = CreateContent(Status.Draft, false); + var command = new UpdateContent { Data = new NamedContentData() }; + + await GuardContent.CanUpdate(content, contentWorkflow, command); + } + + [Fact] + public async Task CanPatch_should_throw_exception_if_data_is_null() + { + SetupSingleton(false); + SetupCanUpdate(true); + + var content = CreateContent(Status.Draft, false); var command = new PatchContent(); - ValidationAssert.Throws(() => GuardContent.CanPatch(command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command), new ValidationError("Data is required.", "Data")); } [Fact] - public void CanPatch_should_not_throw_exception_if_data_is_not_null() + public async Task CanPatch_should_throw_exception_if_workflow_blocks_it() { SetupSingleton(false); + SetupCanUpdate(false); + var content = CreateContent(Status.Draft, false); var command = new PatchContent { Data = new NamedContentData() }; - GuardContent.CanPatch(command); + await Assert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command)); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_status_not_valid() + public async Task CanPatch_should_not_throw_exception_if_data_is_not_null() { SetupSingleton(false); + SetupCanUpdate(true); - var command = new ChangeContentStatus { Status = (Status)10 }; + var content = CreateContent(Status.Draft, false); + var command = new PatchContent { Data = new NamedContentData() }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Archived, command), - new ValidationError("Status is not a valid value.", "Status")); + await GuardContent.CanPatch(content, contentWorkflow, command); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid() + public async Task CanChangeStatus_should_throw_exception_if_publishing_without_pending_changes() { SetupSingleton(false); + var content = CreateContent(Status.Published, false); var command = new ChangeContentStatus { Status = Status.Published }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Archived, command), - new ValidationError("Cannot change status from Archived to Published.", "Status")); + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + new ValidationError("Content has no changes to publish.", "Status")); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_due_date_in_past() + public async Task CanChangeStatus_should_throw_exception_if_singleton() { - SetupSingleton(false); + SetupSingleton(true); - var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast }; + var content = CreateContent(Status.Published, false); + var command = new ChangeContentStatus { Status = Status.Draft }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Draft, command), - new ValidationError("Due time must be in the future.", "DueTime")); + await Assert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command)); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_publishing_without_pending_changes() + public async Task CanChangeStatus_should_not_throw_exception_if_publishing_with_pending_changes() { - SetupSingleton(false); + SetupSingleton(true); + var content = CreateContent(Status.Published, true); var command = new ChangeContentStatus { Status = Status.Published }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Published, command), - new ValidationError("Content has no changes to publish.", "Status")); + await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_singleton() + public async Task CanChangeStatus_should_throw_exception_if_due_date_in_past() { - SetupSingleton(true); + SetupSingleton(false); - var command = new ChangeContentStatus { Status = Status.Draft }; + var content = CreateContent(Status.Draft, false); + var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast }; + + A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + .Returns(true); - Assert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Published, command)); + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + new ValidationError("Due time must be in the future.", "DueTime")); } [Fact] - public void CanChangeContentStatus_should_not_throw_exception_if_publishing_with_pending_changes() + public async Task CanChangeStatus_should_throw_exception_if_status_flow_not_valid() { - SetupSingleton(true); + SetupSingleton(false); + var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published }; - GuardContent.CanChangeContentStatus(schema, true, Status.Published, command); + A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + .Returns(false); + + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + new ValidationError("Cannot change status from Draft to Published.", "Status")); } [Fact] - public void CanChangeContentStatus_should_not_throw_exception_if_status_flow_valid() + public async Task CanChangeStatus_should_not_throw_exception_if_status_flow_valid() { SetupSingleton(false); + var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published }; - GuardContent.CanChangeContentStatus(schema, false, Status.Draft, command); + A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + .Returns(true); + + await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command); } [Fact] @@ -218,10 +256,26 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard GuardContent.CanDelete(schema, command); } + private void SetupCanUpdate(bool canUpdate) + { + A.CallTo(() => contentWorkflow.CanUpdateAsync(A.Ignored)) + .Returns(canUpdate); + } + private void SetupSingleton(bool isSingleton) { A.CallTo(() => schema.SchemaDef) .Returns(new Schema("schema", isSingleton: isSingleton)); } + + private IContentEntity CreateContent(Status status, bool isPending) + { + var content = A.Fake(); + + A.CallTo(() => content.Status).Returns(status); + A.CallTo(() => content.IsPending).Returns(isPending); + + return content; + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs index ce50fc208..40556eb16 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { private sealed class TestObject { - public Status2 Status { get; set; } + public Status Status { get; set; } } [Fact] @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var source = new TestObject { - Status = new Status2("Published") + Status = Status.Published }; var document = new BsonDocument(); From 987c6e4550b068337075ca0a3477ac0d6669d431 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 19 Jun 2019 15:55:54 +0200 Subject: [PATCH 07/10] Method renamed. --- .../Contents/DefaultContentWorkflow.cs | 2 +- .../Contents/Guards/GuardContent.cs | 2 +- .../Contents/IContentWorkflow.cs | 2 +- .../Contents/ContentGrainTests.cs | 4 ++-- .../Contents/DefaultContentWorkflowTests.cs | 2 +- .../Contents/Guard/GuardContentTests.cs | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index b1928480a..131bbe6e8 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return Task.FromResult(Status.Draft); } - public Task IsValidNextStatus(IContentEntity content, Status next) + public Task CanMoveToAsync(IContentEntity content, Status next) { return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next)); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 35a3acca7..4395ffd11 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards return Validate.It(() => "Cannot change status.", async e => { - if (!await contentWorkflow.IsValidNextStatus(content, command.Status)) + if (!await contentWorkflow.CanMoveToAsync(content, command.Status)) { if (content.Status == command.Status && content.Status == Status.Published) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index 06de365be..c812f8a4f 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { Task GetInitialStatusAsync(ISchemaEntity schema); - Task IsValidNextStatus(IContentEntity content, Status next); + Task CanMoveToAsync(IContentEntity content, Status next); Task CanUpdateAsync(IContentEntity content); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs index 8edba81dd..01da99c81 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentWorkflow.CanUpdateAsync(A.Ignored)) .Returns(true); - A.CallTo(() => contentWorkflow.IsValidNextStatus(A.Ignored, A.Ignored)) + A.CallTo(() => contentWorkflow.CanMoveToAsync(A.Ignored, A.Ignored)) .Returns(true); patched = patch.MergeInto(data); @@ -476,7 +476,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new ChangeContentStatus { Status = Status.Draft, JobId = sut.Snapshot.ScheduleJob.Id }; - A.CallTo(() => contentWorkflow.IsValidNextStatus(sut.Snapshot, command.Status)) + A.CallTo(() => contentWorkflow.CanMoveToAsync(sut.Snapshot, command.Status)) .Returns(false); var result = await sut.ExecuteAsync(CreateContentCommand(command)); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index 6a1b5f32a..8c5b35bd1 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var entity = CreateContent(Status.Published); - var result = await sut.IsValidNextStatus(entity, Status.Draft); + var result = await sut.CanMoveToAsync(entity, Status.Draft); Assert.True(result); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs index 1f1eb91cb..93b96243b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast }; - A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + A.CallTo(() => contentWorkflow.CanMoveToAsync(content, command.Status)) .Returns(true); await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), @@ -197,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published }; - A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + A.CallTo(() => contentWorkflow.CanMoveToAsync(content, command.Status)) .Returns(false); await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), @@ -212,7 +212,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published }; - A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + A.CallTo(() => contentWorkflow.CanMoveToAsync(content, command.Status)) .Returns(true); await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command); From e72945b01d36ce49c3bdf5ae9cbf70535f5f7205 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 19 Jun 2019 16:14:53 +0200 Subject: [PATCH 08/10] Small changes finalized. --- .../Contents/StatusChange.cs | 16 ++++++ .../EnrichedContentEventType.cs | 4 +- .../Contents/MongoContentRepository.cs | 1 - .../Contents/ContentChangedTriggerHandler.cs | 17 ++++++- .../Contents/ContentGrain.cs | 27 +++++++--- .../Contents/ContentStatusChanged.cs | 2 + .../pages/content/content-page.component.html | 51 +++++++++---------- .../ContentChangedTriggerHandlerTests.cs | 5 +- .../Contents/ContentGrainTests.cs | 4 +- .../ContentChangedTriggerSchema.cs | 12 +---- 10 files changed, 87 insertions(+), 52 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs new file mode 100644 index 000000000..eae462221 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Contents +{ + public enum StatusChange + { + Change, + Published, + Unpublished + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs index 7ad49332d..565272ef6 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs @@ -11,7 +11,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { Created, Deleted, + Published, StatusChanged, - Updated + Updated, + Unpublished, } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index d28d8078d..999ccf990 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -113,7 +113,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { Guard.NotNull(app, nameof(app)); Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(status, nameof(status)); using (Profiler.TraceMethod()) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 0a155f703..8132c6f28 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -8,6 +8,7 @@ using System; using System.Threading.Tasks; using Orleans; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -59,8 +60,20 @@ namespace Squidex.Domain.Apps.Entities.Contents case ContentUpdated _: result.Type = EnrichedContentEventType.Updated; break; - case ContentStatusChanged _: - result.Type = EnrichedContentEventType.StatusChanged; + case ContentStatusChanged contentStatusChanged: + switch (contentStatusChanged.Change) + { + case StatusChange.Published: + result.Type = EnrichedContentEventType.Published; + break; + case StatusChange.Unpublished: + result.Type = EnrichedContentEventType.Unpublished; + break; + default: + result.Type = EnrichedContentEventType.StatusChanged; + break; + } + break; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 0f73ab461..60af67527 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -127,11 +127,24 @@ namespace Squidex.Domain.Apps.Entities.Contents } else { - var operation = c.Status == Status.Published ? "Published" : "StatusChanged"; - - await ctx.ExecuteScriptAsync(s => s.Change, operation, c, Snapshot.Data); - - ChangeStatus(c); + StatusChange reason; + + if (c.Status == Status.Published) + { + reason = StatusChange.Published; + } + else if (Snapshot.Status == Status.Published) + { + reason = StatusChange.Unpublished; + } + else + { + reason = StatusChange.Change; + } + + await ctx.ExecuteScriptAsync(s => s.Change, reason, c, Snapshot.Data); + + ChangeStatus(c, reason); } } } @@ -261,9 +274,9 @@ namespace Squidex.Domain.Apps.Entities.Contents RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); } - public void ChangeStatus(ChangeContentStatus command) + public void ChangeStatus(ChangeContentStatus command, StatusChange change) { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change })); } private void RaiseEvent(SchemaEvent @event) diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs index 2c97f5902..e7b201ffe 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Events.Contents [EventType(nameof(ContentStatusChanged))] public sealed class ContentStatusChanged : ContentEvent { + public StatusChange Change { get; set; } + public Status Status { get; set; } } } diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 22454ce68..29bbe09de 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -32,8 +32,7 @@ - +