Skip to content

Commit 59c2205

Browse files
authored
Split force for publish descendants into separate options for publish unpublish and re-publish unedited (15) (#18270)
* Port server-side updates from 13 implementation. * Update openapi.json * Update typed client. * Ported over front-end amend from 13. * Handled edge case of publishing invariant root with variant descendants. * Refactor to enum. * Resolved CodeScene warning. * Resolved CodeScene warning. * Resolved CodeScene warning. * Applied suggestions from code review. * Reverted breaking change in integration tests. * Refactored method name.
1 parent ac522ab commit 59c2205

27 files changed

+389
-117
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
using Asp.Versioning;
1+
using Asp.Versioning;
22
using Microsoft.AspNetCore.Authorization;
33
using Microsoft.AspNetCore.Http;
44
using Microsoft.AspNetCore.Mvc;
5-
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
65
using Umbraco.Cms.Api.Management.ViewModels.Document;
76
using Umbraco.Cms.Core;
87
using Umbraco.Cms.Core.Actions;
8+
using Umbraco.Cms.Core.Models;
99
using Umbraco.Cms.Core.Models.ContentPublishing;
1010
using Umbraco.Cms.Core.Security;
1111
using Umbraco.Cms.Core.Security.Authorization;
@@ -53,11 +53,27 @@ public async Task<IActionResult> PublishWithDescendants(CancellationToken cancel
5353
Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus> attempt = await _contentPublishingService.PublishBranchAsync(
5454
id,
5555
requestModel.Cultures,
56-
requestModel.IncludeUnpublishedDescendants,
56+
BuildPublishBranchFilter(requestModel),
5757
CurrentUserKey(_backOfficeSecurityAccessor));
5858

5959
return attempt.Success
6060
? Ok()
6161
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
6262
}
63+
64+
private static PublishBranchFilter BuildPublishBranchFilter(PublishDocumentWithDescendantsRequestModel requestModel)
65+
{
66+
PublishBranchFilter publishBranchFilter = PublishBranchFilter.Default;
67+
if (requestModel.IncludeUnpublishedDescendants)
68+
{
69+
publishBranchFilter |= PublishBranchFilter.IncludeUnpublished;
70+
}
71+
72+
if (requestModel.ForceRepublish)
73+
{
74+
publishBranchFilter |= PublishBranchFilter.ForceRepublish;
75+
}
76+
77+
return publishBranchFilter;
78+
}
6379
}

src/Umbraco.Cms.Api.Management/OpenApi.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42795,13 +42795,17 @@
4279542795
"PublishDocumentWithDescendantsRequestModel": {
4279642796
"required": [
4279742797
"cultures",
42798+
"forceRepublish",
4279842799
"includeUnpublishedDescendants"
4279942800
],
4280042801
"type": "object",
4280142802
"properties": {
4280242803
"includeUnpublishedDescendants": {
4280342804
"type": "boolean"
4280442805
},
42806+
"forceRepublish": {
42807+
"type": "boolean"
42808+
},
4280542809
"cultures": {
4280642810
"type": "array",
4280742811
"items": {
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
1+
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
22

33
public class PublishDocumentWithDescendantsRequestModel
44
{
55
public bool IncludeUnpublishedDescendants { get; set; }
66

7+
public bool ForceRepublish { get; set; }
8+
79
public required IEnumerable<string> Cultures { get; set; }
810
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Umbraco.Cms.Core.Models;
2+
3+
/// <summary>
4+
/// Describes the options available with publishing a content branch for force publishing.
5+
/// </summary>
6+
[Flags]
7+
public enum PublishBranchFilter
8+
{
9+
/// <summary>
10+
/// The default behavior is to publish only the published content that has changed.
11+
/// </summary>
12+
Default = 0,
13+
14+
/// <summary>
15+
/// For publishing a branch, publish all changed content, including content that is not published.
16+
/// </summary>
17+
IncludeUnpublished = 1,
18+
19+
/// <summary>
20+
/// For publishing a branch, force republishing of all published content, including content that has not changed.
21+
/// </summary>
22+
ForceRepublish = 2,
23+
24+
/// <summary>
25+
/// For publishing a branch, publish all content, including content that is not published and content that has not changed.
26+
/// </summary>
27+
All = IncludeUnpublished | ForceRepublish,
28+
}

src/Umbraco.Core/Services/ContentPublishingService.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,12 @@ private async Task<ContentValidationResult> ValidateCurrentContentAsync(IContent
239239
}
240240

241241
/// <inheritdoc />
242+
[Obsolete("This method is not longer used as the 'force' parameter has been split into publishing unpublished and force re-published. Please use the overload containing parameters for those options instead. Will be removed in V17.")]
242243
public async Task<Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus>> PublishBranchAsync(Guid key, IEnumerable<string> cultures, bool force, Guid userKey)
244+
=> await PublishBranchAsync(key, cultures, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, userKey);
245+
246+
/// <inheritdoc />
247+
public async Task<Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus>> PublishBranchAsync(Guid key, IEnumerable<string> cultures, PublishBranchFilter publishBranchFilter, Guid userKey)
243248
{
244249
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
245250
IContent? content = _contentService.GetById(key);
@@ -260,7 +265,7 @@ public async Task<Attempt<ContentPublishingBranchResult, ContentPublishingOperat
260265
}
261266

262267
var userId = await _userIdKeyResolver.GetAsync(userKey);
263-
IEnumerable<PublishResult> result = _contentService.PublishBranch(content, force, cultures.ToArray(), userId);
268+
IEnumerable<PublishResult> result = _contentService.PublishBranch(content, publishBranchFilter, cultures.ToArray(), userId);
264269
scope.Complete();
265270

266271
var itemResults = result.ToDictionary(r => r.Content.Key, ToContentPublishingOperationStatus);

src/Umbraco.Core/Services/ContentService.cs

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,15 +2009,15 @@ private bool PublishBranch_PublishCultures(IContent content, HashSet<string> cul
20092009
&& _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant());
20102010
}
20112011

2012-
// utility 'ShouldPublish' func used by SaveAndPublishBranch
2013-
private static HashSet<string>? PublishBranch_ShouldPublish(ref HashSet<string>? cultures, string c, bool published, bool edited, bool isRoot, bool force)
2012+
// utility 'ShouldPublish' func used by PublishBranch
2013+
private static HashSet<string>? PublishBranch_ShouldPublish(ref HashSet<string>? cultures, string c, bool published, bool edited, bool isRoot, PublishBranchFilter publishBranchFilter)
20142014
{
20152015
// if published, republish
20162016
if (published)
20172017
{
20182018
cultures ??= new HashSet<string>(); // empty means 'already published'
20192019

2020-
if (edited)
2020+
if (edited || publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish))
20212021
{
20222022
cultures.Add(c); // <culture> means 'republish this culture'
20232023
}
@@ -2026,7 +2026,7 @@ private bool PublishBranch_PublishCultures(IContent content, HashSet<string> cul
20262026
}
20272027

20282028
// if not published, publish if force/root else do nothing
2029-
if (!force && !isRoot)
2029+
if (!publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) && !isRoot)
20302030
{
20312031
return cultures; // null means 'nothing to do'
20322032
}
@@ -2054,16 +2054,18 @@ public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool fo
20542054
var isRoot = c.Id == content.Id;
20552055
HashSet<string>? culturesToPublish = null;
20562056

2057+
PublishBranchFilter publishBranchFilter = force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default;
2058+
20572059
// invariant content type
20582060
if (!c.ContentType.VariesByCulture())
20592061
{
2060-
return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
2062+
return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter);
20612063
}
20622064

20632065
// variant content type, specific culture
20642066
if (culture != "*")
20652067
{
2066-
return PublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
2068+
return PublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, publishBranchFilter);
20672069
}
20682070

20692071
// variant content type, all cultures
@@ -2073,7 +2075,7 @@ public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool fo
20732075
// others will have to 'republish this culture'
20742076
foreach (var x in c.AvailableCultures)
20752077
{
2076-
PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
2078+
PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, publishBranchFilter);
20772079
}
20782080

20792081
return culturesToPublish;
@@ -2085,23 +2087,31 @@ public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool fo
20852087
: null; // null means 'nothing to do'
20862088
}
20872089

2088-
return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId);
2090+
return PublishBranch(content, ShouldPublish, PublishBranch_PublishCultures, userId);
20892091
}
20902092

20912093
[Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(PublishBranch)} instead. Will be removed in V16")]
20922094
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId)
2093-
=> PublishBranch(content, force, cultures, userId);
2095+
=> PublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId);
20942096

20952097
/// <inheritdoc />
2098+
[Obsolete("This method is not longer used as the 'force' parameter has been split into publishing unpublished and force re-published. Please use the overload containing parameters for those options instead. Will be removed in V16")]
20962099
public IEnumerable<PublishResult> PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId)
2100+
=> PublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId);
2101+
2102+
/// <inheritdoc />
2103+
public IEnumerable<PublishResult> PublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId)
20972104
{
20982105
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
20992106
// and not to == them, else we would be comparing references, and that is a bad thing
2100-
cultures ??= Array.Empty<string>();
21012107

2102-
if (content.ContentType.VariesByCulture() is false && cultures.Length == 0)
2108+
cultures = EnsureCultures(content, cultures);
2109+
2110+
string? defaultCulture;
2111+
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
21032112
{
2104-
cultures = new[] { "*" };
2113+
defaultCulture = _languageRepository.GetDefaultIsoCode();
2114+
scope.Complete();
21052115
}
21062116

21072117
// determines cultures to be published
@@ -2114,34 +2124,50 @@ public IEnumerable<PublishResult> PublishBranch(IContent content, bool force, st
21142124
// invariant content type
21152125
if (!c.ContentType.VariesByCulture())
21162126
{
2117-
return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
2127+
return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter);
21182128
}
21192129

21202130
// variant content type, specific cultures
21212131
if (c.Published)
21222132
{
21232133
// then some (and maybe all) cultures will be 'already published' (unless forcing),
21242134
// others will have to 'republish this culture'
2125-
foreach (var x in cultures)
2135+
foreach (var culture in cultures)
21262136
{
2127-
PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
2137+
// We could be publishing a parent invariant page, with descendents that are variant.
2138+
// So convert the invariant request to a request for the default culture.
2139+
var specificCulture = culture == "*" ? defaultCulture : culture;
2140+
2141+
PublishBranch_ShouldPublish(ref culturesToPublish, specificCulture, c.IsCulturePublished(specificCulture), c.IsCultureEdited(specificCulture), isRoot, publishBranchFilter);
21282142
}
21292143

21302144
return culturesToPublish;
21312145
}
21322146

2133-
// if not published, publish if force/root else do nothing
2134-
return force || isRoot
2147+
// if not published, publish if forcing unpublished/root else do nothing
2148+
return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot
21352149
? new HashSet<string>(cultures) // means 'publish specified cultures'
21362150
: null; // null means 'nothing to do'
21372151
}
21382152

2139-
return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId);
2153+
return PublishBranch(content, ShouldPublish, PublishBranch_PublishCultures, userId);
21402154
}
21412155

2156+
private static string[] EnsureCultures(IContent content, string[] cultures)
2157+
{
2158+
// Ensure consistent indication of "all cultures" for variant content.
2159+
if (content.ContentType.VariesByCulture() is false && ProvidedCulturesIndicatePublishAll(cultures))
2160+
{
2161+
cultures = ["*"];
2162+
}
2163+
2164+
return cultures;
2165+
}
2166+
2167+
private static bool ProvidedCulturesIndicatePublishAll(string[] cultures) => cultures.Length == 0 || (cultures.Length == 1 && cultures[0] == "invariant");
2168+
21422169
internal IEnumerable<PublishResult> PublishBranch(
21432170
IContent document,
2144-
bool force,
21452171
Func<IContent, HashSet<string>?> shouldPublish,
21462172
Func<IContent, HashSet<string>, IReadOnlyCollection<ILanguage>, bool> publishCultures,
21472173
int userId = Constants.Security.SuperUserId)
@@ -3116,7 +3142,7 @@ internal IEnumerable<IContent> GetPublishedDescendantsLocked(IContent content)
31163142
{
31173143
var pathMatch = content.Path + ",";
31183144
IQuery<IContent> query = Query<IContent>()
3119-
.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/);
3145+
.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& culture.Trashed == false*/);
31203146
IEnumerable<IContent> contents = _documentRepository.Get(query);
31213147

31223148
// beware! contents contains all published version below content

src/Umbraco.Core/Services/IContentPublishingService.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Umbraco.Cms.Core.Models;
44
using Umbraco.Cms.Core.Models.ContentPublishing;
55
using Umbraco.Cms.Core.Services.OperationStatus;
6+
using static Umbraco.Cms.Core.Constants.Conventions;
67

78
namespace Umbraco.Cms.Core.Services;
89

@@ -26,8 +27,22 @@ public interface IContentPublishingService
2627
/// <param name="force">A value indicating whether to force-publish content that is not already published.</param>
2728
/// <param name="userKey">The identifier of the user performing the operation.</param>
2829
/// <returns>Result of the publish operation.</returns>
30+
[Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead. Will be removed in V17.")]
2931
Task<Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus>> PublishBranchAsync(Guid key, IEnumerable<string> cultures, bool force, Guid userKey);
3032

33+
/// <summary>
34+
/// Publishes a content branch.
35+
/// </summary>
36+
/// <param name="key">The key of the root content.</param>
37+
/// <param name="cultures">The cultures to publish.</param>
38+
/// <param name="publishBranchFilter">A value indicating options for force publishing unpublished or re-publishing unchanged content.</param>
39+
/// <param name="userKey">The identifier of the user performing the operation.</param>
40+
/// <returns>Result of the publish operation.</returns>
41+
Task<Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus>> PublishBranchAsync(Guid key, IEnumerable<string> cultures, PublishBranchFilter publishBranchFilter, Guid userKey)
42+
#pragma warning disable CS0618 // Type or member is obsolete
43+
=> PublishBranchAsync(key, cultures, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), userKey);
44+
#pragma warning restore CS0618 // Type or member is obsolete
45+
3146
/// <summary>
3247
/// Unpublishes multiple cultures of a single content item.
3348
/// </summary>

src/Umbraco.Core/Services/IContentService.cs

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -419,32 +419,25 @@ public interface IContentService : IContentServiceBase<IContent>
419419
/// published. The root of the branch is always published, regardless of <paramref name="force" />.
420420
/// </para>
421421
/// </remarks>
422+
[Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead. Will be removed in V17.")]
422423
IEnumerable<PublishResult> PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
423424

424-
///// <summary>
425-
///// Saves and publishes a document branch.
426-
///// </summary>
427-
///// <param name="content">The root document.</param>
428-
///// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
429-
///// <param name="shouldPublish">A function determining cultures to publish.</param>
430-
///// <param name="publishCultures">A function publishing cultures.</param>
431-
///// <param name="userId">The identifier of the user performing the operation.</param>
432-
///// <remarks>
433-
///// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
434-
///// only those documents that are already published, are republished. When <c>true</c>, all documents are
435-
///// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
436-
///// <para>The <paramref name="editing"/> parameter is a function which determines whether a document has
437-
///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
438-
///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
439-
///// cultures may trigger an unwanted republish.</para>
440-
///// <para>The <paramref name="publishCultures"/> parameter is a function to execute to publish cultures, on
441-
///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
442-
///// whether the cultures could be published.</para>
443-
///// </remarks>
444-
// IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force,
445-
// Func<IContent, HashSet<string>> shouldPublish,
446-
// Func<IContent, HashSet<string>, bool> publishCultures,
447-
// int userId = Constants.Security.SuperUserId);
425+
/// <summary>
426+
/// Publishes a document branch.
427+
/// </summary>
428+
/// <param name="content">The root document.</param>
429+
/// <param name="publishBranchFilter">A value indicating options for force publishing unpublished or re-publishing unchanged content.</param>
430+
/// <param name="cultures">The cultures to publish.</param>
431+
/// <param name="userId">The identifier of the user performing the operation.</param>
432+
/// <remarks>
433+
/// <para>
434+
/// The root of the branch is always published, regardless of <paramref name="publishBranchFilter" />.
435+
/// </para>
436+
/// </remarks>
437+
IEnumerable<PublishResult> PublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId)
438+
#pragma warning disable CS0618 // Type or member is obsolete
439+
=> SaveAndPublishBranch(content, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), cultures, userId);
440+
#pragma warning restore CS0618 // Type or member is obsolete
448441

449442
/// <summary>
450443
/// Unpublishes a document.

0 commit comments

Comments
 (0)