diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs index 67b9c69008..62f0699342 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs @@ -1,11 +1,11 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Security.Authorization; @@ -53,11 +53,27 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase Attempt attempt = await _contentPublishingService.PublishBranchAsync( id, requestModel.Cultures, - requestModel.IncludeUnpublishedDescendants, + BuildPublishBranchFilter(requestModel), CurrentUserKey(_backOfficeSecurityAccessor)); return attempt.Success ? Ok() : DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems); } + + private static PublishBranchFilter BuildPublishBranchFilter(PublishDocumentWithDescendantsRequestModel requestModel) + { + PublishBranchFilter publishBranchFilter = PublishBranchFilter.Default; + if (requestModel.IncludeUnpublishedDescendants) + { + publishBranchFilter |= PublishBranchFilter.IncludeUnpublished; + } + + if (requestModel.ForceRepublish) + { + publishBranchFilter |= PublishBranchFilter.ForceRepublish; + } + + return publishBranchFilter; + } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 8e46876b21..9ff09a431a 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -42795,6 +42795,7 @@ "PublishDocumentWithDescendantsRequestModel": { "required": [ "cultures", + "forceRepublish", "includeUnpublishedDescendants" ], "type": "object", @@ -42802,6 +42803,9 @@ "includeUnpublishedDescendants": { "type": "boolean" }, + "forceRepublish": { + "type": "boolean" + }, "cultures": { "type": "array", "items": { diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs index 755626f7f2..2557adf9a4 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs @@ -1,8 +1,10 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.Document; +namespace Umbraco.Cms.Api.Management.ViewModels.Document; public class PublishDocumentWithDescendantsRequestModel { public bool IncludeUnpublishedDescendants { get; set; } + public bool ForceRepublish { get; set; } + public required IEnumerable Cultures { get; set; } } diff --git a/src/Umbraco.Core/Models/PublishBranchFilter.cs b/src/Umbraco.Core/Models/PublishBranchFilter.cs new file mode 100644 index 0000000000..e47a07f677 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishBranchFilter.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Cms.Core.Models; + +/// +/// Describes the options available with publishing a content branch for force publishing. +/// +[Flags] +public enum PublishBranchFilter +{ + /// + /// The default behavior is to publish only the published content that has changed. + /// + Default = 0, + + /// + /// For publishing a branch, publish all changed content, including content that is not published. + /// + IncludeUnpublished = 1, + + /// + /// For publishing a branch, force republishing of all published content, including content that has not changed. + /// + ForceRepublish = 2, + + /// + /// For publishing a branch, publish all content, including content that is not published and content that has not changed. + /// + All = IncludeUnpublished | ForceRepublish, +} diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 0c5ec59e75..bd8eab7f8e 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -239,7 +239,12 @@ internal sealed class ContentPublishingService : IContentPublishingService } /// + [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.")] public async Task> PublishBranchAsync(Guid key, IEnumerable cultures, bool force, Guid userKey) + => await PublishBranchAsync(key, cultures, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, userKey); + + /// + public async Task> PublishBranchAsync(Guid key, IEnumerable cultures, PublishBranchFilter publishBranchFilter, Guid userKey) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); IContent? content = _contentService.GetById(key); @@ -260,7 +265,7 @@ internal sealed class ContentPublishingService : IContentPublishingService } var userId = await _userIdKeyResolver.GetAsync(userKey); - IEnumerable result = _contentService.PublishBranch(content, force, cultures.ToArray(), userId); + IEnumerable result = _contentService.PublishBranch(content, publishBranchFilter, cultures.ToArray(), userId); scope.Complete(); var itemResults = result.ToDictionary(r => r.Content.Key, ToContentPublishingOperationStatus); diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index ce1a724e44..0ee1084252 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -2009,15 +2009,15 @@ public class ContentService : RepositoryService, IContentService && _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant()); } - // utility 'ShouldPublish' func used by SaveAndPublishBranch - private static HashSet? PublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, bool force) + // utility 'ShouldPublish' func used by PublishBranch + private static HashSet? PublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, PublishBranchFilter publishBranchFilter) { // if published, republish if (published) { cultures ??= new HashSet(); // empty means 'already published' - if (edited) + if (edited || publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish)) { cultures.Add(c); // means 'republish this culture' } @@ -2026,7 +2026,7 @@ public class ContentService : RepositoryService, IContentService } // if not published, publish if force/root else do nothing - if (!force && !isRoot) + if (!publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) && !isRoot) { return cultures; // null means 'nothing to do' } @@ -2054,16 +2054,18 @@ public class ContentService : RepositoryService, IContentService var isRoot = c.Id == content.Id; HashSet? culturesToPublish = null; + PublishBranchFilter publishBranchFilter = force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default; + // invariant content type if (!c.ContentType.VariesByCulture()) { - return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter); } // variant content type, specific culture if (culture != "*") { - return PublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); + return PublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, publishBranchFilter); } // variant content type, all cultures @@ -2073,7 +2075,7 @@ public class ContentService : RepositoryService, IContentService // others will have to 'republish this culture' foreach (var x in c.AvailableCultures) { - PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, publishBranchFilter); } return culturesToPublish; @@ -2085,23 +2087,31 @@ public class ContentService : RepositoryService, IContentService : null; // null means 'nothing to do' } - return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId); + return PublishBranch(content, ShouldPublish, PublishBranch_PublishCultures, userId); } [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(PublishBranch)} instead. Will be removed in V16")] public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId) - => PublishBranch(content, force, cultures, userId); + => PublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId); /// + [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")] public IEnumerable PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId) + => PublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId); + + /// + public IEnumerable PublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing - cultures ??= Array.Empty(); - if (content.ContentType.VariesByCulture() is false && cultures.Length == 0) + cultures = EnsureCultures(content, cultures); + + string? defaultCulture; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - cultures = new[] { "*" }; + defaultCulture = _languageRepository.GetDefaultIsoCode(); + scope.Complete(); } // determines cultures to be published @@ -2114,7 +2124,7 @@ public class ContentService : RepositoryService, IContentService // invariant content type if (!c.ContentType.VariesByCulture()) { - return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter); } // variant content type, specific cultures @@ -2122,26 +2132,42 @@ public class ContentService : RepositoryService, IContentService { // then some (and maybe all) cultures will be 'already published' (unless forcing), // others will have to 'republish this culture' - foreach (var x in cultures) + foreach (var culture in cultures) { - PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + // We could be publishing a parent invariant page, with descendents that are variant. + // So convert the invariant request to a request for the default culture. + var specificCulture = culture == "*" ? defaultCulture : culture; + + PublishBranch_ShouldPublish(ref culturesToPublish, specificCulture, c.IsCulturePublished(specificCulture), c.IsCultureEdited(specificCulture), isRoot, publishBranchFilter); } return culturesToPublish; } - // if not published, publish if force/root else do nothing - return force || isRoot + // if not published, publish if forcing unpublished/root else do nothing + return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot ? new HashSet(cultures) // means 'publish specified cultures' : null; // null means 'nothing to do' } - return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId); + return PublishBranch(content, ShouldPublish, PublishBranch_PublishCultures, userId); } + private static string[] EnsureCultures(IContent content, string[] cultures) + { + // Ensure consistent indication of "all cultures" for variant content. + if (content.ContentType.VariesByCulture() is false && ProvidedCulturesIndicatePublishAll(cultures)) + { + cultures = ["*"]; + } + + return cultures; + } + + private static bool ProvidedCulturesIndicatePublishAll(string[] cultures) => cultures.Length == 0 || (cultures.Length == 1 && cultures[0] == "invariant"); + internal IEnumerable PublishBranch( IContent document, - bool force, Func?> shouldPublish, Func, IReadOnlyCollection, bool> publishCultures, int userId = Constants.Security.SuperUserId) @@ -3116,7 +3142,7 @@ public class ContentService : RepositoryService, IContentService { var pathMatch = content.Path + ","; IQuery query = Query() - .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/); + .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& culture.Trashed == false*/); IEnumerable contents = _documentRepository.Get(query); // beware! contents contains all published version below content diff --git a/src/Umbraco.Core/Services/IContentPublishingService.cs b/src/Umbraco.Core/Services/IContentPublishingService.cs index 73fc668543..bf41028977 100644 --- a/src/Umbraco.Core/Services/IContentPublishingService.cs +++ b/src/Umbraco.Core/Services/IContentPublishingService.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; using Umbraco.Cms.Core.Services.OperationStatus; +using static Umbraco.Cms.Core.Constants.Conventions; namespace Umbraco.Cms.Core.Services; @@ -26,8 +27,22 @@ public interface IContentPublishingService /// A value indicating whether to force-publish content that is not already published. /// The identifier of the user performing the operation. /// Result of the publish operation. + [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.")] Task> PublishBranchAsync(Guid key, IEnumerable cultures, bool force, Guid userKey); + /// + /// Publishes a content branch. + /// + /// The key of the root content. + /// The cultures to publish. + /// A value indicating options for force publishing unpublished or re-publishing unchanged content. + /// The identifier of the user performing the operation. + /// Result of the publish operation. + Task> PublishBranchAsync(Guid key, IEnumerable cultures, PublishBranchFilter publishBranchFilter, Guid userKey) +#pragma warning disable CS0618 // Type or member is obsolete + => PublishBranchAsync(key, cultures, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), userKey); +#pragma warning restore CS0618 // Type or member is obsolete + /// /// Unpublishes multiple cultures of a single content item. /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index dbc9868003..0009a0d053 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -419,32 +419,25 @@ public interface IContentService : IContentServiceBase /// published. The root of the branch is always published, regardless of . /// /// + [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.")] IEnumerable PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId); - ///// - ///// Saves and publishes a document branch. - ///// - ///// The root document. - ///// A value indicating whether to force-publish documents that are not already published. - ///// A function determining cultures to publish. - ///// A function publishing cultures. - ///// The identifier of the user performing the operation. - ///// - ///// The parameter determines which documents are published. When false, - ///// only those documents that are already published, are republished. When true, all documents are - ///// published. The root of the branch is always published, regardless of . - ///// The parameter is a function which determines whether a document has - ///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of - ///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other - ///// cultures may trigger an unwanted republish. - ///// The parameter is a function to execute to publish cultures, on - ///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating - ///// whether the cultures could be published. - ///// - // IEnumerable SaveAndPublishBranch(IContent content, bool force, - // Func> shouldPublish, - // Func, bool> publishCultures, - // int userId = Constants.Security.SuperUserId); + /// + /// Publishes a document branch. + /// + /// The root document. + /// A value indicating options for force publishing unpublished or re-publishing unchanged content. + /// The cultures to publish. + /// The identifier of the user performing the operation. + /// + /// + /// The root of the branch is always published, regardless of . + /// + /// + IEnumerable PublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId) +#pragma warning disable CS0618 // Type or member is obsolete + => SaveAndPublishBranch(content, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), cultures, userId); +#pragma warning restore CS0618 // Type or member is obsolete /// /// Unpublishes a document. diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 7cd7b8dbeb..39b7cda55a 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -319,6 +319,7 @@ export default { removeTextBox: 'Remove this text box', contentRoot: 'Content root', includeUnpublished: 'Include unpublished content items.', + forceRepublish: 'Publish unchanged items.', isSensitiveValue: 'This value is hidden. If you need access to view this value please contact your\n website administrator.\n ', isSensitiveValue_short: 'This value is hidden.', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index cb184bbd3f..bace39178c 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -315,6 +315,7 @@ export default { removeTextBox: 'Remove this text box', contentRoot: 'Content root', includeUnpublished: 'Include unpublished content items.', + forceRepublish: 'Publish unchanged items.', isSensitiveValue: 'This value is hidden. If you need access to view this value please contact your\n website administrator.\n ', isSensitiveValue_short: 'This value is hidden.', diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts index 41a57432ea..be5966855a 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts @@ -2045,6 +2045,7 @@ export type PublishDocumentRequestModel = { export type PublishDocumentWithDescendantsRequestModel = { includeUnpublishedDescendants: boolean; + forceRepublish: boolean; cultures: Array<(string)>; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts index 032ac944e5..8bd1dd817a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts @@ -18,6 +18,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE > { #selectionManager = new UmbSelectionManager(this); #includeUnpublishedDescendants = false; + #forceRepublish = false; @state() _options: Array = []; @@ -73,6 +74,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE this.value = { selection: this.#selectionManager.getSelection(), includeUnpublishedDescendants: this.#includeUnpublishedDescendants, + forceRepublish: this.#forceRepublish, }; this.modalContext?.submit(); } @@ -113,6 +115,14 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE @change=${() => (this.#includeUnpublishedDescendants = !this.#includeUnpublishedDescendants)}> + + (this.#forceRepublish = !this.#forceRepublish)}> + +
, includeUnpublishedDescendants: boolean) { + async publishWithDescendants(id: string, variantIds: Array, includeUnpublishedDescendants: boolean, forceRepublish: boolean) { if (!id) throw new Error('id is missing'); if (!variantIds) throw new Error('variant IDs are missing'); await this.#init; @@ -84,6 +85,7 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase { id, variantIds, includeUnpublishedDescendants, + forceRepublish, ); if (!error) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts index 9c8d088d7f..320397438f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts @@ -92,18 +92,21 @@ export class UmbDocumentPublishingServerDataSource { * @param unique * @param variantIds * @param includeUnpublishedDescendants + * @param forceRepublish * @memberof UmbDocumentPublishingServerDataSource */ async publishWithDescendants( unique: string, variantIds: Array, includeUnpublishedDescendants: boolean, + forceRepublish: boolean, ) { if (!unique) throw new Error('Id is missing'); const requestBody: PublishDocumentWithDescendantsRequestModel = { cultures: variantIds.map((variant) => variant.toCultureString()), includeUnpublishedDescendants, + forceRepublish, }; return tryExecuteAndNotify( diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts index 3ee552845e..f54cc30bc9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts @@ -161,6 +161,7 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase x.Success), Is.False); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs index 500b7bdc0d..cfc775c88e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs @@ -66,7 +66,7 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent [Test] public async Task Deleted_documents_do_not_have_a_url_segment__Parent_deleted() { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); ContentService.Delete(Textpage); @@ -80,7 +80,7 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent [Test] public async Task Deleted_documents_do_not_have_a_url_segment() { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); ContentService.Delete(Subpage2); @@ -104,7 +104,7 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent { if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); } return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); @@ -121,7 +121,7 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent public void Unpublished_Pages_Are_not_available() { //Arrange - ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); Assert.Multiple(() => { @@ -161,7 +161,7 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); } return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); @@ -180,8 +180,8 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); + ContentService.PublishBranch(secondRoot, PublishBranchFilter.IncludeUnpublished, ["*"]); } return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); @@ -206,8 +206,8 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); + ContentService.PublishBranch(secondRoot, PublishBranchFilter.IncludeUnpublished, ["*"]); } return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs index 41e3f18979..5efc52f778 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs @@ -50,7 +50,7 @@ public class DocumentUrlServiceTest_HideTopLevel_False : UmbracoIntegrationTestW { if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); } @@ -70,7 +70,7 @@ public class DocumentUrlServiceTest_HideTopLevel_False : UmbracoIntegrationTestW if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); } return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); @@ -89,8 +89,8 @@ public class DocumentUrlServiceTest_HideTopLevel_False : UmbracoIntegrationTestW if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); + ContentService.PublishBranch(secondRoot, PublishBranchFilter.IncludeUnpublished, ["*"]); } return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); @@ -114,8 +114,8 @@ public class DocumentUrlServiceTest_HideTopLevel_False : UmbracoIntegrationTestW // Publish both the main root and the second root with descendants if (loadDraft is false) { - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); + ContentService.PublishBranch(secondRoot, PublishBranchFilter.IncludeUnpublished, ["*"]); } return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs index 3580d0314b..3fbf0b4628 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs @@ -67,7 +67,7 @@ public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent }); // Act - var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" }); + var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); await sut.InitializeAsync(CancellationToken.None); Assert.Multiple(() => @@ -105,7 +105,7 @@ public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture)); // Act - var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" }); + var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); await sut.AddOrUpdateStatusWithDescendantsAsync(Textpage.Key, CancellationToken.None); Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture)); Assert.IsTrue(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Updated due to being an descendant @@ -126,7 +126,7 @@ public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture)); // Act - var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" }); + var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); await sut.AddOrUpdateStatusAsync(Textpage.Key, CancellationToken.None); Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture)); Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Not updated @@ -167,7 +167,7 @@ public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); ContentService.Save(grandchild, -1, contentSchedule); - var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" }); + var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); var randomCulture = "da-DK"; var subPage2FromDB = ContentService.GetById(Subpage2.Key); @@ -190,7 +190,7 @@ public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent [Test] public void When_Branch_is_publised_default_language_return_true() { - var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" }); + var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); var randomCulture = "da-DK"; Assert.Multiple(() => { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs index 0b73743b8a..2fa0fccbac 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Tests.Common.Builders; @@ -21,8 +21,8 @@ public class PublishedUrlInfoProviderTests : PublishedUrlInfoProviderTestsBase ContentService.Save(childOfSecondRoot, -1, contentSchedule); // Publish both the main root and the second root with descendants - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); + ContentService.PublishBranch(secondRoot, PublishBranchFilter.IncludeUnpublished, ["*"]); var subPageUrls = await PublishedUrlInfoProvider.GetAllAsync(Subpage); var childOfSecondRootUrls = await PublishedUrlInfoProvider.GetAllAsync(childOfSecondRoot); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs index b0f1a03ec1..cc68a4bdb0 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; @@ -28,8 +28,8 @@ public class PublishedUrlInfoProvider_hidetoplevel_false : PublishedUrlInfoProvi ContentService.Save(childOfSecondRoot, -1, contentSchedule); // Publish both the main root and the second root with descendants - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); + ContentService.PublishBranch(secondRoot, PublishBranchFilter.IncludeUnpublished, ["*"]); var subPageUrls = await PublishedUrlInfoProvider.GetAllAsync(Subpage); var childOfSecondRootUrls = await PublishedUrlInfoProvider.GetAllAsync(childOfSecondRoot); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs index 2e329e664b..9ed8c37f15 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs @@ -719,7 +719,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // branch is: ResetEvents(); - ContentService.PublishBranch(content1, force: false, cultures: content1.AvailableCultures.ToArray()); // force = false, don't publish unpublished items + ContentService.PublishBranch(content1, PublishBranchFilter.Default, cultures: content1.AvailableCultures.ToArray()); // PublishBranchFilter.Default: don't publish unpublished items foreach (EventInstance e in _events) { @@ -756,7 +756,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services ContentService.Unpublish(content1); ResetEvents(); - ContentService.PublishBranch(content1, force: true, cultures: content1.AvailableCultures.ToArray()); // force = true, also publish unpublished items + ContentService.PublishBranch(content1, PublishBranchFilter.IncludeUnpublished, cultures: content1.AvailableCultures.ToArray()); // PublishBranchFilter.IncludeUnpublished: also publish unpublished items foreach (EventInstance e in _events) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs index e7a740084d..db0bf3658d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs @@ -42,17 +42,25 @@ public partial class ContentPublishingServiceTests VerifyIsPublished(Subpage.Key); } - [TestCase(true)] - [TestCase(false)] - public async Task Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Explicitly_Instructed_To(bool force) + [Obsolete("Replaced by Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Instructed_To. This will be removed in Umbraco 16.")] + public Task Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Explicitly_Instructed_To(bool force) { - var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, force, Constants.Security.SuperUserKey); + return Task.CompletedTask; + } + + [TestCase(PublishBranchFilter.Default)] + [TestCase(PublishBranchFilter.IncludeUnpublished)] + [TestCase(PublishBranchFilter.ForceRepublish)] + [TestCase(PublishBranchFilter.All)] + public async Task Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Instructed_To(PublishBranchFilter publishBranchFilter) + { + var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, publishBranchFilter, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); VerifyIsPublished(Textpage.Key); - if (force) + if (publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished)) { AssertBranchResultSuccess(result.Result, Textpage.Key, Subpage.Key, Subpage2.Key, Subpage3.Key); VerifyIsPublished(Subpage.Key); @@ -76,7 +84,7 @@ public partial class ContentPublishingServiceTests ContentService.Save(subpage2Subpage, -1); VerifyIsNotPublished(Subpage2.Key); - var result = await ContentPublishingService.PublishBranchAsync(Subpage2.Key, _allCultures, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(Subpage2.Key, _allCultures, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); AssertBranchResultSuccess(result.Result, Subpage2.Key, subpage2Subpage.Key); @@ -187,7 +195,7 @@ public partial class ContentPublishingServiceTests child.SetValue("title", "DA child title", culture: langDa.IsoCode); ContentService.Save(child); - var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode, langDa.IsoCode }, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode, langDa.IsoCode }, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); AssertBranchResultSuccess(result.Result, root.Key, child.Key); @@ -249,7 +257,7 @@ public partial class ContentPublishingServiceTests child.SetValue("title", "DA child title", culture: langDa.IsoCode); ContentService.Save(child); - var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode }, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode }, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); AssertBranchResultSuccess(result.Result, root.Key, child.Key); @@ -288,7 +296,7 @@ public partial class ContentPublishingServiceTests child.SetValue("title", "DA child title", culture: langDa.IsoCode); ContentService.Save(child); - var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode }, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode }, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); AssertBranchResultSuccess(result.Result, root.Key, child.Key); @@ -335,7 +343,7 @@ public partial class ContentPublishingServiceTests public async Task Cannot_Publish_Branch_Of_Non_Existing_Content() { var key = Guid.NewGuid(); - var result = await ContentPublishingService.PublishBranchAsync(key, _allCultures, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(key, _allCultures, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsFalse(result); AssertBranchResultFailed(result.Result, (key, ContentPublishingOperationStatus.ContentNotFound)); } @@ -367,7 +375,7 @@ public partial class ContentPublishingServiceTests ContentService.Save(child, -1); Assert.AreEqual(content.Id, ContentService.GetById(child.Key)!.ParentId); - var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); AssertBranchResultSuccess(result.Result, Textpage.Key, Subpage.Key, Subpage2.Key, Subpage3.Key); @@ -448,7 +456,7 @@ public partial class ContentPublishingServiceTests child.SetValue("title", "DA child title", culture: langDa.IsoCode); ContentService.Save(child); - var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode, langDa.IsoCode }, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode, langDa.IsoCode }, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); AssertBranchResultFailed(result.Result, (root.Key, ContentPublishingOperationStatus.ContentInvalid)); @@ -541,7 +549,7 @@ public partial class ContentPublishingServiceTests [Test] public async Task Cannot_Republish_Branch_After_Adding_Mandatory_Property() { - var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, true, Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); VerifyIsPublished(Textpage.Key); VerifyIsPublished(Subpage.Key); @@ -571,7 +579,7 @@ public partial class ContentPublishingServiceTests textPage.SetValue("mandatoryProperty", "This is a valid value"); ContentService.Save(textPage); - result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, true, Constants.Security.SuperUserKey); + result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, PublishBranchFilter.IncludeUnpublished, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentPublishingOperationStatus.FailedBranch, result.Status); AssertBranchResultSuccess(result.Result, Textpage.Key); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs index ff906251fa..eeb67fab8d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -48,9 +46,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // ii1 !published !edited // ii2 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.Default = publishes those that are actually published, and have changes // here: root (root is always published) - var r = PublishInvariantBranch(iRoot, false, method).ToArray(); + var r = PublishInvariantBranch(iRoot, PublishBranchFilter.Default, method).ToArray(); // not forcing, ii1 and ii2 not published yet: only root got published AssertPublishResults(r, x => x.Content.Name, "iroot"); @@ -86,9 +84,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // ii21 (published) !edited // ii22 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.Default = publishes those that are actually published, and have changes // here: nothing - r = PublishInvariantBranch(iRoot, false, method).ToArray(); + r = PublishInvariantBranch(iRoot, PublishBranchFilter.Default, method).ToArray(); // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "ii11"); @@ -113,11 +111,11 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // ii21 (published) !edited // ii22 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.Default = publishes those that are actually published, and have changes // here: iroot and ii11 // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published - r = PublishInvariantBranch(iRoot, false, method).ToArray(); + r = PublishInvariantBranch(iRoot, PublishBranchFilter.Default, method).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "ii11"); AssertPublishResults( r, @@ -126,9 +124,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublish); - // force = publishes everything that has changes + // PublishBranchFilter.IncludeUnpublished = publishes everything that has changes // here: ii12, ii2, ii22 - ii21 was published already but masked - r = PublishInvariantBranch(iRoot, true, method).ToArray(); + r = PublishInvariantBranch(iRoot, PublishBranchFilter.IncludeUnpublished, method).ToArray(); AssertPublishResults( r, x => x.Content.Name, @@ -187,7 +185,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest iv1.SetValue("vp", "UPDATED-iv1.de", "de"); ContentService.Save(iv1); - var r = ContentService.PublishBranch(vRoot, false, vRoot.AvailableCultures.ToArray()) + var r = ContentService.PublishBranch(vRoot, PublishBranchFilter.Default, vRoot.AvailableCultures.ToArray()) .ToArray(); // no culture specified so "*" is used, so all cultures Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); @@ -226,7 +224,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest iv1.SetValue("vp", "UPDATED-iv1.de", "de"); var saveResult = ContentService.Save(iv1); - var r = ContentService.PublishBranch(vRoot, false, new [] { "de" }).ToArray(); + var r = ContentService.PublishBranch(vRoot, PublishBranchFilter.Default, ["de"]).ToArray(); Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); } @@ -270,9 +268,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // iv1 !published !edited // iv2 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.Default = publishes those that are actually published, and have changes // here: nothing - var r = ContentService.PublishBranch(vRoot, false, new[] { "*" }).ToArray(); // no culture specified = all cultures + var r = ContentService.PublishBranch(vRoot, PublishBranchFilter.Default, ["*"]).ToArray(); // no culture specified = all cultures // not forcing, iv1 and iv2 not published yet: only root got published AssertPublishResults(r, x => x.Content.Name, "vroot.de"); @@ -305,7 +303,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest Assert.IsTrue(iv1.IsCulturePublished("ru")); Assert.IsFalse(iv1.IsCulturePublished("es")); - r = ContentService.PublishBranch(vRoot, false, new[] { "de" }).ToArray(); + r = ContentService.PublishBranch(vRoot, PublishBranchFilter.Default, ["de"]).ToArray(); // not forcing, iv2 not published yet: only root and iv1 got published AssertPublishResults(r, x => x.Content.Name, "vroot.de", "iv1.de"); @@ -384,7 +382,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest { Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); - var r = ContentService.PublishBranch(iRoot, false, new[] { "de" }).ToArray(); + var r = ContentService.PublishBranch(iRoot, PublishBranchFilter.Default, ["de"]).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "iv11.de"); AssertPublishResults( r, @@ -410,7 +408,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest { Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); - var r = ContentService.PublishBranch(iRoot, false, new[] { "de", "ru" }).ToArray(); + var r = ContentService.PublishBranch(iRoot, PublishBranchFilter.Default, ["de", "ru"]).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "iv11.de"); AssertPublishResults( r, @@ -431,6 +429,163 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest Assert.AreEqual("changed.ru", iv11.GetValue("vp", "ru", published: true)); } + [TestCase(PublishBranchFilter.Default)] + [TestCase(PublishBranchFilter.IncludeUnpublished)] + [TestCase(PublishBranchFilter.ForceRepublish)] + [TestCase(PublishBranchFilter.All)] + public void Can_Publish_Invariant_Branch_With_Force_Options(PublishBranchFilter publishBranchFilter) + { + CreateTypes(out var iContentType, out _); + + // Create content (published root, published child, unpublished child, changed child). + IContent iRoot = new Content("iroot", -1, iContentType); + iRoot.SetValue("ip", "iroot"); + ContentService.Save(iRoot); + ContentService.Publish(iRoot, iRoot.AvailableCultures.ToArray()); + + IContent ii1 = new Content("ii1", iRoot, iContentType); + ii1.SetValue("ip", "vii1"); + ContentService.Save(ii1); + ContentService.Publish(ii1, ii1.AvailableCultures.ToArray()); + + IContent ii2 = new Content("ii2", iRoot, iContentType); + ii2.SetValue("ip", "vii2"); + ContentService.Save(ii2); + + IContent ii3 = new Content("ii3", iRoot, iContentType); + ii3.SetValue("ip", "vii3"); + ContentService.Save(ii3); + ContentService.Publish(ii3, ii3.AvailableCultures.ToArray()); + ii3.SetValue("ip", "vii3a"); + ContentService.Save(ii3); + + var result = ContentService.PublishBranch(iRoot, publishBranchFilter, ["*"]).ToArray(); + + var expectedContentNames = GetExpectedContentNamesForForceOptions(publishBranchFilter); + var expectedPublishResultTypes = GetExpectedPublishResultTypesForForceOptions(publishBranchFilter); + AssertPublishResults(result, x => x.Content.Name, expectedContentNames); + AssertPublishResults( + result, + x => x.Result, + expectedPublishResultTypes); + } + + [TestCase("*", PublishBranchFilter.Default)] + [TestCase("*", PublishBranchFilter.IncludeUnpublished)] + [TestCase("*", PublishBranchFilter.ForceRepublish)] + [TestCase("*", PublishBranchFilter.All)] + [TestCase("de", PublishBranchFilter.Default)] + [TestCase("de", PublishBranchFilter.IncludeUnpublished)] + [TestCase("de", PublishBranchFilter.ForceRepublish)] + [TestCase("de", PublishBranchFilter.All)] + public void Can_Publish_Variant_Branch_With_Force_Options(string culture, PublishBranchFilter publishBranchFilter) + { + CreateTypes(out _, out var vContentType); + + // Create content (published root, published child, unpublished child, changed child). + IContent vRoot = new Content("vroot", -1, vContentType); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + ContentService.Save(vRoot); + ContentService.Publish(vRoot, vRoot.AvailableCultures.ToArray()); + + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + ContentService.Save(iv1); + ContentService.Publish(iv1, iv1.AvailableCultures.ToArray()); + + IContent iv2 = new Content("iv2", vRoot, vContentType, "de"); + iv2.SetCultureName("iv2.de", "de"); + iv2.SetCultureName("iv2.ru", "ru"); + iv2.SetValue("ip", "iv2"); + iv2.SetValue("vp", "iv2.de", "de"); + iv2.SetValue("vp", "iv2.ru", "ru"); + ContentService.Save(iv2); + + // When testing with a specific culture, publish the other one, so we can test that + // the specified unpublished culture is handled correctly. + if (culture != "*") + { + ContentService.Publish(iv2, ["ru"]); + } + + IContent iv3 = new Content("iv3", vRoot, vContentType, "de"); + iv3.SetCultureName("iv3.de", "de"); + iv3.SetCultureName("iv3.ru", "ru"); + iv3.SetValue("ip", "iv3"); + iv3.SetValue("vp", "iv3.de", "de"); + iv3.SetValue("vp", "iv3.ru", "ru"); + ContentService.Save(iv3); + ContentService.Publish(iv3, iv3.AvailableCultures.ToArray()); + iv3.SetValue("ip", "iv3a"); + iv3.SetValue("vp", "iv3a.de", "de"); + iv3.SetValue("vp", "iv3a.ru", "ru"); + ContentService.Save(iv3); + + var cultures = culture == "*" ? vRoot.AvailableCultures.ToArray() : new[] { culture }; + var result = ContentService.PublishBranch(vRoot, publishBranchFilter, cultures).ToArray(); + + var expectedContentNames = GetExpectedContentNamesForForceOptions(publishBranchFilter, true); + var expectedPublishResultTypes = GetExpectedPublishResultTypesForForceOptions(publishBranchFilter, true); + AssertPublishResults(result, x => x.Content.Name, expectedContentNames); + AssertPublishResults( + result, + x => x.Result, + expectedPublishResultTypes); + } + + private static string[] GetExpectedContentNamesForForceOptions(PublishBranchFilter options, bool isVariant = false) + { + var rootName = isVariant ? "vroot.de" : "iroot"; + var childPrefix = isVariant ? "iv" : "ii"; + var childSuffix = isVariant ? ".de" : string.Empty; + if (options.HasFlag(PublishBranchFilter.IncludeUnpublished)) + { + return [rootName, $"{childPrefix}1{childSuffix}", $"{childPrefix}2{childSuffix}", $"{childPrefix}3{childSuffix}"]; + } + + return [rootName, $"{childPrefix}1{childSuffix}", $"{childPrefix}3{childSuffix}"]; + } + + private static PublishResultType[] GetExpectedPublishResultTypesForForceOptions(PublishBranchFilter options, bool isVariant = false) + { + var successPublish = isVariant ? PublishResultType.SuccessPublishCulture : PublishResultType.SuccessPublish; + if (options.HasFlag(PublishBranchFilter.IncludeUnpublished) && options.HasFlag(PublishBranchFilter.ForceRepublish)) + { + return [successPublish, + successPublish, + successPublish, + successPublish]; + } + + if (options.HasFlag(PublishBranchFilter.IncludeUnpublished)) + { + return [PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + successPublish, + successPublish]; + } + + if (options.HasFlag(PublishBranchFilter.ForceRepublish)) + { + return [successPublish, + successPublish, + + successPublish]; + } + + return [PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + successPublish]; + } + private void AssertPublishResults(PublishResult[] values, Func getter, params T[] expected) { if (expected.Length != values.Length) @@ -488,18 +643,18 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest ContentTypeService.Save(vContentType); } - private IEnumerable PublishInvariantBranch(IContent content, bool force, int method) + private IEnumerable PublishInvariantBranch(IContent content, PublishBranchFilter publishBranchFilter, int method) { // ReSharper disable RedundantArgumentDefaultValue // ReSharper disable ArgumentsStyleOther switch (method) { case 1: - return ContentService.PublishBranch(content, force, content.AvailableCultures.ToArray()); + return ContentService.PublishBranch(content, publishBranchFilter, content.AvailableCultures.ToArray()); case 2: - return ContentService.PublishBranch(content, force, cultures: new[] { "*" }); + return ContentService.PublishBranch(content, publishBranchFilter, cultures: ["*"]); case 3: - return ContentService.PublishBranch(content, force, cultures: Array.Empty()); + return ContentService.PublishBranch(content, publishBranchFilter, cultures: Array.Empty()); default: throw new ArgumentOutOfRangeException(nameof(method)); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs index bcab65dadf..33ce4b3ff2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs @@ -686,7 +686,7 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest ContentService.Save(child2); // Act - ContentService.PublishBranch(content, true, content.AvailableCultures.ToArray()); + ContentService.PublishBranch(content, PublishBranchFilter.IncludeUnpublished, content.AvailableCultures.ToArray()); // Assert var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id;