V14: Unpublish multiple cultures (#15789)

* Refactor to accept multiple cultures

* Add test

* Move logic into service

* Remember to complete scope

* Move scope creation.

* Add test

* Close scope on early return

* Handle invalid cultures with bad request

* Handle valid cultures based on content

* Change return type if a culture is required

* Refactor content publishing service to have 1 unpublish method

* Update tests

* Return better error

* Scope completes

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Nikolaj Geisle
2024-02-29 14:38:47 +01:00
committed by GitHub
parent 10c18e4162
commit b8577e3af2
11 changed files with 194 additions and 23 deletions

View File

@@ -91,6 +91,14 @@ public abstract class DocumentControllerBase : ContentControllerBase
.WithTitle("Culture missing")
.WithDetail("A culture needs to be specified to execute the operation.")
.Build()),
ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant => BadRequest(problemDetailsBuilder
.WithTitle("Cannot publish invariant when variant")
.WithDetail("Cannot publish invariant culture when the document varies by culture.")
.Build()),
ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant => BadRequest(problemDetailsBuilder
.WithTitle("Cannot publish variant when not variant.")
.WithDetail("Cannot publish a given culture when the document is invariant.")
.Build()),
ContentPublishingOperationStatus.ConcurrencyViolation => BadRequest(problemDetailsBuilder
.WithTitle("Concurrency violation detected")
.WithDetail("An attempt was made to publish a version older than the latest version.")

View File

@@ -39,13 +39,12 @@ public class UnpublishDocumentController : DocumentControllerBase
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Unpublish(Guid id, UnpublishDocumentRequestModel requestModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(
ActionUnpublish.ActionLetter,
id,
requestModel.Culture is not null ? requestModel.Culture.Yield() : Enumerable.Empty<string>()),
requestModel.Cultures ?? Enumerable.Empty<string>()),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
@@ -55,8 +54,9 @@ public class UnpublishDocumentController : DocumentControllerBase
Attempt<ContentPublishingOperationStatus> attempt = await _contentPublishingService.UnpublishAsync(
id,
requestModel.Culture,
requestModel.Cultures,
CurrentUserKey(_backOfficeSecurityAccessor));
return attempt.Success
? Ok()
: DocumentPublishingOperationStatusResult(attempt.Result);

View File

@@ -39602,8 +39602,12 @@
"UnpublishDocumentRequestModel": {
"type": "object",
"properties": {
"culture": {
"type": "string",
"cultures": {
"uniqueItems": true,
"type": "array",
"items": {
"type": "string"
},
"nullable": true
}
},

View File

@@ -2,5 +2,5 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class UnpublishDocumentRequestModel
{
public string? Culture { get; set; }
public ISet<string>? Cultures { get; set; }
}

View File

@@ -380,7 +380,7 @@ public static class ContentRepositoryExtensions
var keepProcessing = true;
if (culture == "*")
if (culture == "*" || (content.ContentType.VariesByCulture() is false && culture is null))
{
// all cultures
content.ClearPublishInfos();

View File

@@ -197,24 +197,123 @@ internal sealed class ContentPublishingService : IContentPublishingService
: Attempt.FailWithStatus(ContentPublishingOperationStatus.FailedBranch, branchResult);
}
/// <inheritdoc />
public async Task<Attempt<ContentPublishingOperationStatus>> UnpublishAsync(Guid key, string? culture, Guid userKey)
/// <inheritdoc/>
public async Task<Attempt<ContentPublishingOperationStatus>> UnpublishAsync(Guid key, ISet<string>? cultures, Guid userKey)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
IContent? content = _contentService.GetById(key);
if (content is null)
{
scope.Complete();
return Attempt.Fail(ContentPublishingOperationStatus.ContentNotFound);
}
var userId = await _userIdKeyResolver.GetAsync(userKey);
PublishResult result = _contentService.Unpublish(content, culture ?? "*", userId);
Attempt<ContentPublishingOperationStatus> attempt;
if (cultures is null)
{
attempt = await UnpublishInvariantAsync(
content,
userId);
scope.Complete();
return attempt;
}
if (cultures.Any() is false)
{
scope.Complete();
return Attempt<ContentPublishingOperationStatus>.Fail(ContentPublishingOperationStatus.CultureMissing);
}
if (cultures.Contains("*"))
{
attempt = await UnpublishAllCulturesAsync(
content,
userId);
}
else
{
attempt = await UnpublishMultipleCultures(
content,
cultures,
userId);
}
scope.Complete();
return attempt;
}
private Task<Attempt<ContentPublishingOperationStatus>> UnpublishAllCulturesAsync(IContent content, int userId)
{
if (content.ContentType.VariesByCulture() is false)
{
return Task.FromResult(Attempt.Fail(ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant));
}
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
PublishResult result = _contentService.Unpublish(content, "*", userId);
scope.Complete();
ContentPublishingOperationStatus contentPublishingOperationStatus = ToContentPublishingOperationStatus(result);
return contentPublishingOperationStatus is ContentPublishingOperationStatus.Success
return Task.FromResult(contentPublishingOperationStatus is ContentPublishingOperationStatus.Success
? Attempt.Succeed(ToContentPublishingOperationStatus(result))
: Attempt.Fail(ToContentPublishingOperationStatus(result));
: Attempt.Fail(ToContentPublishingOperationStatus(result)));
}
private async Task<Attempt<ContentPublishingOperationStatus>> UnpublishMultipleCultures(IContent content, ISet<string> cultures, int userId)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
if (content.ContentType.VariesByCulture() is false)
{
scope.Complete();
return Attempt.Fail(ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant);
}
var validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray();
foreach (var culture in cultures)
{
if (validCultures.Contains(culture, StringComparer.InvariantCultureIgnoreCase) is false)
{
scope.Complete();
return Attempt.Fail(ContentPublishingOperationStatus.InvalidCulture);
}
PublishResult result = _contentService.Unpublish(content, culture, userId);
ContentPublishingOperationStatus contentPublishingOperationStatus = ToContentPublishingOperationStatus(result);
if (contentPublishingOperationStatus is not ContentPublishingOperationStatus.Success)
{
return Attempt.Fail(ToContentPublishingOperationStatus(result));
}
}
scope.Complete();
return Attempt.Succeed(ContentPublishingOperationStatus.Success);
}
private Task<Attempt<ContentPublishingOperationStatus>> UnpublishInvariantAsync(IContent content, int userId)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
if (content.ContentType.VariesByCulture())
{
return Task.FromResult(Attempt.Fail(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant));
}
PublishResult result = _contentService.Unpublish(content, null, userId);
scope.Complete();
ContentPublishingOperationStatus contentPublishingOperationStatus = ToContentPublishingOperationStatus(result);
return Task.FromResult(contentPublishingOperationStatus is ContentPublishingOperationStatus.Success
? Attempt.Succeed(ToContentPublishingOperationStatus(result))
: Attempt.Fail(ToContentPublishingOperationStatus(result)));
}
private static ContentPublishingOperationStatus ToContentPublishingOperationStatus(PublishResult publishResult)

View File

@@ -1,5 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
@@ -26,11 +25,11 @@ public interface IContentPublishingService
Task<Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus>> PublishBranchAsync(Guid key, IEnumerable<string> cultures, bool force, Guid userKey);
/// <summary>
/// Unpublishes a single content item.
/// Unpublishes multiple cultures of a single content item.
/// </summary>
/// <param name="key">The key of the root content.</param>
/// <param name="culture">The culture to unpublish. Use null to unpublish all cultures.</param>
/// <param name="cultures">The cultures to unpublish. Use null to unpublish all cultures.</param>
/// <param name="userKey">The identifier of the user performing the operation.</param>
/// <returns>Status of the publish operation.</returns>
Task<Attempt<ContentPublishingOperationStatus>> UnpublishAsync(Guid key, string? culture, Guid userKey);
Task<Attempt<ContentPublishingOperationStatus>> UnpublishAsync(Guid key, ISet<string>? cultures, Guid userKey);
}

View File

@@ -441,7 +441,7 @@ public interface IContentService : IContentServiceBase<IContent>
/// empty. If the content type is invariant, then culture can be either '*' or null or empty.
/// </para>
/// </remarks>
PublishResult Unpublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
PublishResult Unpublish(IContent content, string? culture = "*", int userId = Constants.Security.SuperUserId);
/// <summary>
/// Gets a value indicating whether a document is path-publishable.

View File

@@ -14,6 +14,8 @@ public enum ContentPublishingOperationStatus
CultureAwaitingRelease,
InTrash,
InvalidCulture,
CannotPublishInvariantWhenVariant,
CannotPublishVariantWhenNotVariant,
CultureMissing,
PathNotPublished,
ConcurrencyViolation,