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:
@@ -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.")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -39602,8 +39602,12 @@
|
||||
"UnpublishDocumentRequestModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"culture": {
|
||||
"type": "string",
|
||||
"cultures": {
|
||||
"uniqueItems": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -14,6 +14,8 @@ public enum ContentPublishingOperationStatus
|
||||
CultureAwaitingRelease,
|
||||
InTrash,
|
||||
InvalidCulture,
|
||||
CannotPublishInvariantWhenVariant,
|
||||
CannotPublishVariantWhenNotVariant,
|
||||
CultureMissing,
|
||||
PathNotPublished,
|
||||
ConcurrencyViolation,
|
||||
|
||||
@@ -145,7 +145,7 @@ public partial class ContentPublishingServiceTests
|
||||
Assert.IsTrue(content.PublishedCultures.InvariantContains(langEn.IsoCode));
|
||||
Assert.IsTrue(content.PublishedCultures.InvariantContains(langDa.IsoCode));
|
||||
|
||||
var unpublishResult = await ContentPublishingService.UnpublishAsync(content.Key, "*", Constants.Security.SuperUserKey);
|
||||
var unpublishResult = await ContentPublishingService.UnpublishAsync(content.Key, new HashSet<string>() { "*" }, Constants.Security.SuperUserKey);
|
||||
|
||||
Assert.IsTrue(unpublishResult.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, unpublishResult.Result);
|
||||
|
||||
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
|
||||
|
||||
@@ -107,7 +108,7 @@ public partial class ContentPublishingServiceTests
|
||||
await ContentPublishingService.PublishAsync(content.Key, MakeModel(new HashSet<string>() { langEn.IsoCode, langDa.IsoCode }), Constants.Security.SuperUserKey);
|
||||
VerifyIsPublished(content.Key);
|
||||
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, langEn.IsoCode, Constants.Security.SuperUserKey);
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, new HashSet<string> { langEn.IsoCode }, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
|
||||
VerifyIsPublished(content.Key);
|
||||
@@ -133,7 +134,7 @@ public partial class ContentPublishingServiceTests
|
||||
await ContentPublishingService.PublishAsync(content.Key, MakeModel(new HashSet<string>() { langEn.IsoCode, langDa.IsoCode }), Constants.Security.SuperUserKey);
|
||||
VerifyIsPublished(content.Key);
|
||||
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, null, Constants.Security.SuperUserKey);
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, new HashSet<string>(){"*"}, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
|
||||
VerifyIsNotPublished(content.Key);
|
||||
@@ -142,6 +143,64 @@ public partial class ContentPublishingServiceTests
|
||||
Assert.AreEqual(0, content.PublishedCultures.Count());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Unpublish_All_Cultures_With_Multiple_Cultures_Method()
|
||||
{
|
||||
var (langEn, langDa, contentType) = await SetupVariantTest();
|
||||
|
||||
IContent content = new ContentBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithCultureName(langEn.IsoCode, "EN root")
|
||||
.WithCultureName(langDa.IsoCode, "DA root")
|
||||
.Build();
|
||||
content.SetValue("title", "EN title", culture: langEn.IsoCode);
|
||||
content.SetValue("title", "DA title", culture: langDa.IsoCode);
|
||||
ContentService.Save(content);
|
||||
await ContentPublishingService.PublishAsync(content.Key, MakeModel(new HashSet<string>() { langEn.IsoCode, langDa.IsoCode }), Constants.Security.SuperUserKey);
|
||||
VerifyIsPublished(content.Key);
|
||||
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, new HashSet<string>() { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
|
||||
VerifyIsNotPublished(content.Key);
|
||||
|
||||
content = ContentService.GetById(content.Key)!;
|
||||
Assert.AreEqual(0, content.PublishedCultures.Count());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Unpublish_Multiple_Cultures()
|
||||
{
|
||||
var (langEn, langDa, contentType) = await SetupVariantTest();
|
||||
|
||||
var langSe = new LanguageBuilder()
|
||||
.WithCultureInfo("sv-SE")
|
||||
.Build();
|
||||
await LanguageService.CreateAsync(langSe, Constants.Security.SuperUserKey);
|
||||
|
||||
IContent content = new ContentBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithCultureName(langEn.IsoCode, "EN root")
|
||||
.WithCultureName(langDa.IsoCode, "DA root")
|
||||
.WithCultureName(langSe.IsoCode, "SE root")
|
||||
.Build();
|
||||
content.SetValue("title", "EN title", culture: langEn.IsoCode);
|
||||
content.SetValue("title", "DA title", culture: langDa.IsoCode);
|
||||
content.SetValue("title", "SE title", culture: langSe.IsoCode);
|
||||
ContentService.Save(content);
|
||||
await ContentPublishingService.PublishAsync(content.Key, MakeModel(new HashSet<string>() { langEn.IsoCode, langDa.IsoCode, langSe.IsoCode }), Constants.Security.SuperUserKey);
|
||||
VerifyIsPublished(content.Key);
|
||||
content = ContentService.GetById(content.Key)!;
|
||||
Assert.AreEqual(3, content.PublishedCultures.Count());
|
||||
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, new HashSet<string> { langDa.IsoCode, langSe.IsoCode }, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
|
||||
VerifyIsPublished(content.Key);
|
||||
content = ContentService.GetById(content.Key)!;
|
||||
Assert.AreEqual(1, content.PublishedCultures.Count());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Unpublish_All_Cultures_By_Unpublishing_Mandatory_Culture()
|
||||
{
|
||||
@@ -161,7 +220,7 @@ public partial class ContentPublishingServiceTests
|
||||
content = ContentService.GetById(content.Key)!;
|
||||
Assert.AreEqual(2, content.PublishedCultures.Count());
|
||||
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, langEn.IsoCode, Constants.Security.SuperUserKey);
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, new HashSet<string> { langEn.IsoCode }, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
|
||||
VerifyIsNotPublished(content.Key);
|
||||
@@ -191,7 +250,7 @@ public partial class ContentPublishingServiceTests
|
||||
content = ContentService.GetById(content.Key)!;
|
||||
Assert.AreEqual(2, content.PublishedCultures.Count());
|
||||
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, langDa.IsoCode, Constants.Security.SuperUserKey);
|
||||
var result = await ContentPublishingService.UnpublishAsync(content.Key, new HashSet<string>() { langDa.IsoCode }, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
|
||||
VerifyIsPublished(content.Key);
|
||||
|
||||
Reference in New Issue
Block a user