Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publishing in the Management API #14774

Merged
merged 29 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
189da55
make CoreScopeProvider available for derived classes
Zeegaan Jul 26, 2023
a2f24a4
Create publish controller
Zeegaan Jul 26, 2023
83f35f8
Add publish functionality
Zeegaan Jul 26, 2023
3914da9
Remove unneeded using
Zeegaan Jul 26, 2023
09ea05b
Implement publish for multiple cultures
Zeegaan Jul 27, 2023
2ddf837
support multiple cultures in controler
Zeegaan Jul 27, 2023
4791fd8
Dont validate properties
Zeegaan Jul 27, 2023
e04d432
Refactor to use PublishingOperationStatus
Zeegaan Jul 27, 2023
25d9138
refactor to use proper publish async methods
Zeegaan Jul 27, 2023
5e20298
Refactor publish logic into own service
Zeegaan Jul 27, 2023
f5cfef1
Commit some demo code
Zeegaan Aug 28, 2023
a35f351
Add notes about what errors can happen when publishing
Zeegaan Aug 29, 2023
25273c8
Rework ContentPublishingService and introduce explicit Publish and Pu…
kjac Sep 4, 2023
73aa60a
Merge branch 'v14/dev' into v14/feature/PublishContentController
kjac Sep 4, 2023
ae1392b
Fix merge
kjac Sep 5, 2023
b5b9264
Allow the publishing strategy to do its job
kjac Sep 6, 2023
6c013a5
Improved check for unsaved changes
kjac Sep 7, 2023
0505101
Make the old content controller work (as best possible)
kjac Sep 7, 2023
a2abd57
Remove SaveAndPublish (SaveAndPublishBranch) from all tests
kjac Sep 7, 2023
69c6438
Proper guards for invalid cultures when publishing
kjac Sep 7, 2023
0db4749
Fix edge cases for property validation and content unpublishing + add…
kjac Sep 11, 2023
8d22ce8
Clear out a few TODOs - we'll accept the behavior for now
kjac Sep 12, 2023
6d0df3a
Unpublish controller
kjac Sep 12, 2023
e278d4d
Merge branch 'v14/dev' into v14/feature/PublishContentController
kjac Oct 17, 2023
bb7c23d
Fix merge
kjac Oct 17, 2023
eab15d6
Fix branch publish notifications
kjac Oct 22, 2023
b19d404
Merge branch 'v14/dev' into v14/feature/PublishContentController
kjac Nov 1, 2023
7b18cc4
Merge branch 'v14/dev' into v14/feature/PublishContentController
kjac Nov 16, 2023
6c057b2
Added extra test for publishing unpublished cultures and added FIXME …
kjac Nov 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion src/Umbraco.Cms.Api.Management/Content/ContentControllerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
.WithDetail("A notification handler prevented the content operation.")
.Build()),
ContentEditingOperationStatus.ContentTypeNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("Cancelled by notification")
.WithTitle("The requested content could not be found")
.Build()),
ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Content type culture variance mismatch")
Expand Down Expand Up @@ -66,6 +66,67 @@
.Build()),
};

protected IActionResult ContentPublishingOperationStatusResult(ContentPublishingOperationStatus status) =>
status switch
{
ContentPublishingOperationStatus.ContentNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("The requested content could not be found")
.Build()),
ContentPublishingOperationStatus.CancelledByEvent => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Publish cancelled by event")
.WithDetail("The publish operation was cancelled by an event.")
.Build()),
ContentPublishingOperationStatus.ContentInvalid => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid content")
.WithDetail("The specified content had an invalid configuration.")
.Build()),
ContentPublishingOperationStatus.NothingToPublish => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Nothing to publish")
.WithDetail("None of the specified cultures needed publishing.")
.Build()),
ContentPublishingOperationStatus.MandatoryCultureMissing => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Mandatory culture missing")
.WithDetail("Must include all mandatory cultures when publishing.")
.Build()),
ContentPublishingOperationStatus.HasExpired => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Content expired")
.WithDetail("Could not publish the content because it was expired.")
.Build()),
ContentPublishingOperationStatus.CultureHasExpired => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Content culture expired")
.WithDetail("Could not publish the content because some of the specified cultures were expired.")
.Build()),
ContentPublishingOperationStatus.AwaitingRelease => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Content awaiting release")
.WithDetail("Could not publish the content because it was awaiting release.")
.Build()),
ContentPublishingOperationStatus.CultureAwaitingRelease => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Content culture awaiting release")
.WithDetail("Could not publish the content because some of the specified cultures were awaiting release.")
.Build()),
ContentPublishingOperationStatus.InTrash => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Content in the recycle bin")
.WithDetail("Could not publish the content because it was in the recycle bin.")
.Build()),
ContentPublishingOperationStatus.PathNotPublished => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Parent not published")
.WithDetail("Could not publish the content because its parent was not published.")
.Build()),
ContentPublishingOperationStatus.ConcurrencyViolation => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Concurrency violation detected")
.WithDetail("An attempt was made to publish a version older than the latest version.")
.Build()),
ContentPublishingOperationStatus.UnsavedChanges => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Unsaved changes")
.WithDetail("Could not publish the content because it had unsaved changes. Make sure to save all changes before attempting a publish.")
.Build()),
ContentPublishingOperationStatus.Failed => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Publish or unpublish failed")
.WithDetail("An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
};

Check warning on line 128 in src/Umbraco.Cms.Api.Management/Content/ContentControllerBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

❌ New issue: Complex Method

ContentPublishingOperationStatusResult has a cyclomatic complexity of 17, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

protected IActionResult ContentCreatingOperationStatusResult(ContentCreatingOperationStatus status) =>
status switch
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.Document;

public class PublishDocumentController : DocumentControllerBase
{
private readonly IContentPublishingService _contentPublishingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public PublishDocumentController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_contentPublishingService = contentPublishingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPut("{id:guid}/publish")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Publish(Guid id, PublishDocumentRequestModel requestModel)
{
Attempt<ContentPublishingOperationStatus> attempt = await _contentPublishingService.PublishAsync(
id,
requestModel.Cultures,
CurrentUserKey(_backOfficeSecurityAccessor));
return attempt.Success
? Ok()
: ContentPublishingOperationStatusResult(attempt.Result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.Document;

public class PublishDocumentWithDescendantsController : DocumentControllerBase
{
private readonly IContentPublishingService _contentPublishingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public PublishDocumentWithDescendantsController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_contentPublishingService = contentPublishingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPut("{id:guid}/publish-with-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> PublishWithDescendants(Guid id, PublishDocumentWithDescendantsRequestModel requestModel)
{
Attempt<IDictionary<Guid, ContentPublishingOperationStatus>> attempt = await _contentPublishingService.PublishBranchAsync(
id,
requestModel.Cultures,
requestModel.IncludeUnpublishedDescendants,
CurrentUserKey(_backOfficeSecurityAccessor));

// FIXME: when we get to implement proper validation handling, this should return a collection of status codes by key (based on attempt.Result)
return attempt.Success
? Ok()
: ContentPublishingOperationStatusResult(
attempt.Result?.Values.FirstOrDefault(r => r is not ContentPublishingOperationStatus.Success)
?? throw new NotSupportedException("The attempt was not successful - at least one result value should be unsuccessful too"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.Document;

public class UnpublishDocumentController : DocumentControllerBase
{
private readonly IContentPublishingService _contentPublishingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public UnpublishDocumentController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_contentPublishingService = contentPublishingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPut("{id:guid}/unpublish")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Unpublish(Guid id, UnpublishDocumentRequestModel requestModel)
{
Attempt<ContentPublishingOperationStatus> attempt = await _contentPublishingService.UnpublishAsync(
id,
requestModel.Culture,
CurrentUserKey(_backOfficeSecurityAccessor));
return attempt.Success
? Ok()
: ContentPublishingOperationStatusResult(attempt.Result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;

public class PublishDocumentRequestModel
{
public required IEnumerable<string> Cultures { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;

public class PublishDocumentWithDescendantsRequestModel : PublishDocumentRequestModel
{
public bool IncludeUnpublishedDescendants { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;

public class UnpublishDocumentRequestModel
{
public string? Culture { get; set; }
}
1 change: 1 addition & 0 deletions src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@
Services.AddUnique<ITagService, TagService>();
Services.AddUnique<IContentService, ContentService>();
Services.AddUnique<IContentEditingService, ContentEditingService>();
Services.AddUnique<IContentPublishingService, ContentPublishingService>();

Check warning on line 306 in src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

❌ Getting worse: Large Method

AddCoreServices increases from 160 to 161 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
Services.AddUnique<IContentCreatingService, ContentCreatingService>();
Services.AddUnique<IContentVersionCleanupPolicy, DefaultContentVersionCleanupPolicy>();
Services.AddUnique<IMemberService, MemberService>();
Expand Down
11 changes: 6 additions & 5 deletions src/Umbraco.Core/Services/ContentEditingServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
private readonly PropertyEditorCollection _propertyEditorCollection;
private readonly IDataTypeService _dataTypeService;
private readonly ILogger<ContentEditingServiceBase<TContent, TContentType, TContentService, TContentTypeService>> _logger;
private readonly ICoreScopeProvider _scopeProvider;
private readonly ITreeEntitySortingService _treeEntitySortingService;
private readonly IUserIdKeyResolver _userIdKeyResolver;

Expand All @@ -35,9 +34,9 @@ protected ContentEditingServiceBase(
_propertyEditorCollection = propertyEditorCollection;
_dataTypeService = dataTypeService;
_logger = logger;
_scopeProvider = scopeProvider;
_userIdKeyResolver = userIdKeyResolver;
_treeEntitySortingService = treeEntitySortingService;
CoreScopeProvider = scopeProvider;
ContentService = contentService;
ContentTypeService = contentTypeService;
}
Expand All @@ -56,6 +55,8 @@ protected ContentEditingServiceBase(

protected abstract ContentEditingOperationStatus Sort(IEnumerable<TContent> items, int userId);

protected ICoreScopeProvider CoreScopeProvider { get; }

protected TContentService ContentService { get; }

protected TContentTypeService ContentTypeService { get; }
Expand Down Expand Up @@ -110,7 +111,7 @@ protected async Task<Attempt<ContentEditingOperationStatus>> MapUpdate(TContent
// helper method to perform move-to-recycle-bin and delete for content as they are very much handled in the same way
private async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeletionAsync(Guid key, Guid userKey, bool mustBeTrashed, Func<TContent, int, OperationResult?> performDelete)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
TContent? content = ContentService.GetById(key);
if (content == null)
{
Expand All @@ -135,7 +136,7 @@ protected async Task<Attempt<ContentEditingOperationStatus>> MapUpdate(TContent

protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleMoveAsync(Guid key, Guid? parentKey, Guid userKey)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
TContent? content = ContentService.GetById(key);
if (content is null)
{
Expand Down Expand Up @@ -172,7 +173,7 @@ protected async Task<Attempt<ContentEditingOperationStatus>> MapUpdate(TContent

protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleCopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
TContent? content = ContentService.GetById(key);
if (content is null)
{
Expand Down
Loading
Loading