Browse Source

Check publishing.

pull/382/head
Sebastian Stehle 7 years ago
parent
commit
af38e08e5c
  1. 2
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs
  2. 14
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs
  4. 2
      src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs
  5. 16
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  6. 6
      src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  7. 15
      src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  8. 7
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  9. 2
      src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  10. 8
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs
  11. 32
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs
  12. 65
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

2
src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs

@ -12,7 +12,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
public abstract class ContentDataCommand : ContentCommand public abstract class ContentDataCommand : ContentCommand
{ {
public NamedContentData Data { get; set; } public NamedContentData Data { get; set; }
public bool AsDraft { get; set; }
} }
} }

14
src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public abstract class ContentUpdateCommand : ContentDataCommand
{
public bool AsDraft { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Contents.Commands namespace Squidex.Domain.Apps.Entities.Contents.Commands
{ {
public sealed class PatchContent : ContentDataCommand public sealed class PatchContent : ContentUpdateCommand
{ {
} }
} }

2
src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Contents.Commands namespace Squidex.Domain.Apps.Entities.Contents.Commands
{ {
public sealed class UpdateContent : ContentDataCommand public sealed class UpdateContent : ContentUpdateCommand
{ {
} }
} }

16
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, Guid.Empty, () => "Failed to create content."); var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, Guid.Empty, () => "Failed to create content.");
GuardContent.CanCreate(ctx.Schema, c); await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c);
await ctx.ExecuteScriptAndTransformAsync(s => s.Create, "Create", c, c.Data); await ctx.ExecuteScriptAndTransformAsync(s => s.Create, "Create", c, c.Data);
await ctx.EnrichAsync(c.Data); await ctx.EnrichAsync(c.Data);
@ -190,9 +190,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
private async Task<object> UpdateAsync(ContentDataCommand c, Func<NamedContentData, NamedContentData> newDataFunc, bool partial) private async Task<object> UpdateAsync(ContentUpdateCommand command, Func<NamedContentData, NamedContentData> newDataFunc, bool partial)
{ {
var isProposal = c.AsDraft && Snapshot.Status == Status.Published; var isProposal = command.AsDraft && Snapshot.Status == Status.Published;
var currentData = var currentData =
isProposal ? isProposal ?
@ -207,22 +207,22 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (partial) if (partial)
{ {
await ctx.ValidatePartialAsync(c.Data); await ctx.ValidatePartialAsync(command.Data);
} }
else else
{ {
await ctx.ValidateAsync(c.Data); await ctx.ValidateAsync(command.Data);
} }
newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, "Update", c, newData, Snapshot.Data); newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, "Update", command, newData, Snapshot.Data);
if (isProposal) if (isProposal)
{ {
ProposeUpdate(c, newData); ProposeUpdate(command, newData);
} }
else else
{ {
Update(c, newData); Update(command, newData);
} }
} }

6
src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs

@ -12,6 +12,7 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
@ -54,6 +55,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user)
{
return TaskHelper.True;
}
public Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) public Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user)
{ {
var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next); var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next);

15
src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs

@ -43,7 +43,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content, user); return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user);
}
public async Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user)
{
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user);
} }
public async Task<bool> CanUpdateAsync(IContentEntity content) public async Task<bool> CanUpdateAsync(IContentEntity content)
@ -87,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) foreach (var (to, step, transition) in workflow.GetTransitions(content.Status))
{ {
if (CanUse(transition, content, user)) if (CanUse(transition, content.DataDraft, user))
{ {
result.Add(new StatusInfo(to, GetColor(step))); result.Add(new StatusInfo(to, GetColor(step)));
} }
@ -96,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return result.ToArray(); return result.ToArray();
} }
private bool CanUse(WorkflowTransition transition, IContentEntity content, ClaimsPrincipal user) private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user)
{ {
if (!string.IsNullOrWhiteSpace(transition.Role)) if (!string.IsNullOrWhiteSpace(transition.Role))
{ {
@ -108,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!string.IsNullOrWhiteSpace(transition.Expression)) if (!string.IsNullOrWhiteSpace(transition.Expression))
{ {
return scriptEngine.Evaluate("data", content.DataDraft, transition.Expression); return scriptEngine.Evaluate("data", data, transition.Expression);
} }
return true; return true;

7
src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{ {
public static class GuardContent public static class GuardContent
{ {
public static void CanCreate(ISchemaEntity schema, CreateContent command) public static async Task CanCreate(ISchemaEntity schema, IContentWorkflow contentWorkflow, CreateContent command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
@ -29,6 +29,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{ {
throw new DomainException("Singleton content cannot be created."); throw new DomainException("Singleton content cannot be created.");
} }
if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User))
{
throw new DomainException("Content workflow prevents publishing.");
}
} }
public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command) public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command)

2
src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema); Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user);
Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user); Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user);
Task<bool> CanUpdateAsync(IContentEntity content); Task<bool> CanUpdateAsync(IContentEntity content);

8
tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs

@ -16,6 +16,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow();
[Fact]
public async Task Should_always_allow_publish_on_create()
{
var result = await sut.CanPublishOnCreateAsync(null, null, null);
Assert.True(result);
}
[Fact] [Fact]
public async Task Should_draft_as_initial_status() public async Task Should_draft_as_initial_status()
{ {

32
tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs

@ -104,6 +104,36 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Should().BeEquivalentTo(expected); result.Should().BeEquivalentTo(expected);
} }
[Fact]
public async Task Should_allow_publish_on_create()
{
var content = CreateContent(Status.Draft, 2);
var result = await sut.CanPublishOnCreateAsync(CreateSchema(), content.DataDraft, User("Editor"));
Assert.True(result);
}
[Fact]
public async Task Should_not_allow_publish_on_create_if_data_is_invalid()
{
var content = CreateContent(Status.Draft, 4);
var result = await sut.CanPublishOnCreateAsync(CreateSchema(), content.DataDraft, User("Editor"));
Assert.False(result);
}
[Fact]
public async Task Should_not_allow_publish_on_create_if_role_not_allowed()
{
var content = CreateContent(Status.Draft, 2);
var result = await sut.CanPublishOnCreateAsync(CreateSchema(), content.DataDraft, User("Developer"));
Assert.False(result);
}
[Fact] [Fact]
public async Task Should_check_is_valid_next() public async Task Should_check_is_valid_next()
{ {
@ -125,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
[Fact] [Fact]
public async Task Should_not_allow_transition_if_expression_does_not_evauate_to_true() public async Task Should_not_allow_transition_if_data_not_valid()
{ {
var content = CreateContent(Status.Draft, 4); var content = CreateContent(Status.Draft, 4);

65
tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

@ -27,51 +27,71 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
private readonly ClaimsPrincipal user = new ClaimsPrincipal(); private readonly ClaimsPrincipal user = new ClaimsPrincipal();
private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1));
[Fact] public GuardContentTests()
public void CanCreate_should_throw_exception_if_data_is_null()
{ {
SetupSingleton(false); SetupSingleton(false);
}
[Fact]
public async Task CanCreate_should_throw_exception_if_data_is_null()
{
var command = new CreateContent(); var command = new CreateContent();
ValidationAssert.Throws(() => GuardContent.CanCreate(schema, command), await ValidationAssert.ThrowsAsync(() => GuardContent.CanCreate(schema, contentWorkflow, command),
new ValidationError("Data is required.", "Data")); new ValidationError("Data is required.", "Data"));
} }
[Fact] [Fact]
public void CanCreate_should_throw_exception_if_singleton() public async Task CanCreate_should_throw_exception_if_singleton()
{ {
SetupSingleton(true); SetupSingleton(true);
var command = new CreateContent { Data = new NamedContentData() }; var command = new CreateContent { Data = new NamedContentData() };
Assert.Throws<DomainException>(() => GuardContent.CanCreate(schema, command)); await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanCreate(schema, contentWorkflow, command));
} }
[Fact] [Fact]
public void CanCreate_should_not_throw_exception_if_singleton_and_id_is_schema_id() public async Task CanCreate_should_not_throw_exception_if_singleton_and_id_is_schema_id()
{ {
SetupSingleton(true); SetupSingleton(true);
var command = new CreateContent { Data = new NamedContentData(), ContentId = schema.Id }; var command = new CreateContent { Data = new NamedContentData(), ContentId = schema.Id };
GuardContent.CanCreate(schema, command); await GuardContent.CanCreate(schema, contentWorkflow, command);
} }
[Fact] [Fact]
public void CanCreate_should_not_throw_exception_if_data_is_not_null() public async Task CanCreate_should_throw_exception_publish_not_allowed()
{ {
SetupSingleton(false); SetupCanCreatePublish(false);
var command = new CreateContent { Data = new NamedContentData(), Publish = true };
await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanCreate(schema, contentWorkflow, command));
}
[Fact]
public async Task CanCreate_should_not_throw_exception_publishing_allowed()
{
SetupCanCreatePublish(true);
var command = new CreateContent { Data = new NamedContentData(), Publish = true };
await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanCreate(schema, contentWorkflow, command));
}
[Fact]
public async Task CanCreate_should_not_throw_exception_if_data_is_not_null()
{
var command = new CreateContent { Data = new NamedContentData() }; var command = new CreateContent { Data = new NamedContentData() };
GuardContent.CanCreate(schema, command); await GuardContent.CanCreate(schema, contentWorkflow, command);
} }
[Fact] [Fact]
public async Task CanUpdate_should_throw_exception_if_data_is_null() public async Task CanUpdate_should_throw_exception_if_data_is_null()
{ {
SetupSingleton(false);
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
@ -84,7 +104,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanUpdate_should_throw_exception_if_workflow_blocks_it() public async Task CanUpdate_should_throw_exception_if_workflow_blocks_it()
{ {
SetupSingleton(false);
SetupCanUpdate(false); SetupCanUpdate(false);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
@ -96,7 +115,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanUpdate_should_not_throw_exception_if_data_is_not_null() public async Task CanUpdate_should_not_throw_exception_if_data_is_not_null()
{ {
SetupSingleton(false);
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
@ -108,7 +126,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanPatch_should_throw_exception_if_data_is_null() public async Task CanPatch_should_throw_exception_if_data_is_null()
{ {
SetupSingleton(false);
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
@ -121,7 +138,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanPatch_should_throw_exception_if_workflow_blocks_it() public async Task CanPatch_should_throw_exception_if_workflow_blocks_it()
{ {
SetupSingleton(false);
SetupCanUpdate(false); SetupCanUpdate(false);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
@ -133,7 +149,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanPatch_should_not_throw_exception_if_data_is_not_null() public async Task CanPatch_should_not_throw_exception_if_data_is_not_null()
{ {
SetupSingleton(false);
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
@ -145,8 +160,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanChangeStatus_should_throw_exception_if_publishing_without_pending_changes() public async Task CanChangeStatus_should_throw_exception_if_publishing_without_pending_changes()
{ {
SetupSingleton(false);
var content = CreateContent(Status.Published, false); var content = CreateContent(Status.Published, false);
var command = new ChangeContentStatus { Status = Status.Published }; var command = new ChangeContentStatus { Status = Status.Published };
@ -179,8 +192,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanChangeStatus_should_throw_exception_if_due_date_in_past() public async Task CanChangeStatus_should_throw_exception_if_due_date_in_past()
{ {
SetupSingleton(false);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast, User = user }; var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast, User = user };
@ -194,8 +205,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanChangeStatus_should_throw_exception_if_status_flow_not_valid() public async Task CanChangeStatus_should_throw_exception_if_status_flow_not_valid()
{ {
SetupSingleton(false);
var content = CreateContent(Status.Draft, false); var content = CreateContent(Status.Draft, false);
var command = new ChangeContentStatus { Status = Status.Published, User = user }; var command = new ChangeContentStatus { Status = Status.Published, User = user };
@ -209,8 +218,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public async Task CanChangeStatus_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 content = CreateContent(Status.Draft, false);
var command = new ChangeContentStatus { Status = Status.Published, User = user }; var command = new ChangeContentStatus { Status = Status.Published, User = user };
@ -231,8 +238,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public void CanDiscardChanges_should_not_throw_exception_if_pending() public void CanDiscardChanges_should_not_throw_exception_if_pending()
{ {
SetupSingleton(false);
var command = new DiscardChanges(); var command = new DiscardChanges();
GuardContent.CanDiscardChanges(true, command); GuardContent.CanDiscardChanges(true, command);
@ -251,8 +256,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
[Fact] [Fact]
public void CanDelete_should_not_throw_exception() public void CanDelete_should_not_throw_exception()
{ {
SetupSingleton(false);
var command = new DeleteContent(); var command = new DeleteContent();
GuardContent.CanDelete(schema, command); GuardContent.CanDelete(schema, command);
@ -264,6 +267,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
.Returns(canUpdate); .Returns(canUpdate);
} }
private void SetupCanCreatePublish(bool canCreate)
{
A.CallTo(() => contentWorkflow.CanPublishOnCreateAsync(schema, A<NamedContentData>.Ignored, user))
.Returns(canCreate);
}
private void SetupSingleton(bool isSingleton) private void SetupSingleton(bool isSingleton)
{ {
A.CallTo(() => schema.SchemaDef) A.CallTo(() => schema.SchemaDef)

Loading…
Cancel
Save