From 49760f6bce0c6a84cdd73830dc983aeb244edff1 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 7 Nov 2024 11:27:56 +0100 Subject: [PATCH 01/16] Expose schedule date for on document get endpoint --- .../Document/ByKeyDocumentController.cs | 22 +++++++++++++++++- .../Factories/DocumentPresentationFactory.cs | 19 +++++++++++++++ .../Factories/IDocumentPresentationFactory.cs | 7 ++++++ .../Mapping/Document/DocumentMapDefinition.cs | 23 +++++++++++++++++++ .../Document/DocumentVariantResponseModel.cs | 4 ++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs index b805633b66b0..32858213cbe3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs @@ -2,10 +2,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services; @@ -20,7 +22,9 @@ public class ByKeyDocumentController : DocumentControllerBase private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IDocumentPresentationFactory _documentPresentationFactory; + private readonly IContentService _contentService; + [Obsolete("Scheduled for removal in v17")] public ByKeyDocumentController( IAuthorizationService authorizationService, IContentEditingService contentEditingService, @@ -29,6 +33,20 @@ public ByKeyDocumentController( _authorizationService = authorizationService; _contentEditingService = contentEditingService; _documentPresentationFactory = documentPresentationFactory; + _contentService = StaticServiceProvider.Instance.GetRequiredService(); + } + + [ActivatorUtilitiesConstructor] + public ByKeyDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IDocumentPresentationFactory documentPresentationFactory, + IContentService contentService) + { + _authorizationService = authorizationService; + _contentEditingService = contentEditingService; + _documentPresentationFactory = documentPresentationFactory; + _contentService = contentService; } [HttpGet("{id:guid}")] @@ -53,7 +71,9 @@ public async Task ByKey(CancellationToken cancellationToken, Guid return DocumentNotFound(); } - DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content); + ContentScheduleCollection schedule = _contentService.GetContentScheduleByContentId(content.Id); + + DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content, schedule); return Ok(model); } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index c6291f4db07d..f1e6e1aad9db 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -37,6 +37,7 @@ public DocumentPresentationFactory( _timeProvider = timeProvider; } + [Obsolete("Schedule for removal in v17")] public async Task CreateResponseModelAsync(IContent content) { DocumentResponseModel responseModel = _umbracoMapper.Map(content)!; @@ -54,6 +55,24 @@ public async Task CreateResponseModelAsync(IContent conte return responseModel; } + public async Task CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule) + { + DocumentResponseModel responseModel = _umbracoMapper.Map(content)!; + _umbracoMapper.Map(schedule, responseModel); + + responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content); + + Guid? templateKey = content.TemplateId.HasValue + ? _templateService.GetAsync(content.TemplateId.Value).Result?.Key + : null; + + responseModel.Template = templateKey.HasValue + ? new ReferenceByIdModel { Id = templateKey.Value } + : null; + + return responseModel; + } + public DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity) { var responseModel = new DocumentItemResponseModel diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs index 23b11b0e4f22..51643a0b7225 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs @@ -12,8 +12,15 @@ namespace Umbraco.Cms.Api.Management.Factories; public interface IDocumentPresentationFactory { + [Obsolete("Schedule for removal in v17")] Task CreateResponseModelAsync(IContent content); + Task CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule) +#pragma warning disable CS0618 // Type or member is obsolete + // Remove when obsolete CreateResponseModelAsync is removed + => CreateResponseModelAsync(content); +#pragma warning restore CS0618 // Type or member is obsolete + DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity); DocumentBlueprintItemResponseModel CreateBlueprintItemResponseModel(IDocumentEntitySlim entity); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs index 494631fe1c26..bf1a859d17d4 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs @@ -24,6 +24,7 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define((_, _) => new DocumentResponseModel(), Map); mapper.Define((_, _) => new DocumentCollectionResponseModel(), Map); mapper.Define((_, _) => new DocumentBlueprintResponseModel(), Map); + mapper.Define(Map); } // Umbraco.Code.MapAll -Urls -Template @@ -90,4 +91,26 @@ private void Map(IContent source, DocumentBlueprintResponseModel target, MapperC documentVariantViewModel.State = DocumentVariantState.Draft; }); } + + private void Map(ContentScheduleCollection source, DocumentResponseModel target, MapperContext context) + { + foreach (ContentSchedule schedule in source.FullSchedule) + { + DocumentVariantResponseModel? variant = target.Variants.FirstOrDefault(v => v.Culture == schedule.Culture || (v.Culture.IsNullOrWhiteSpace() && schedule.Culture.IsNullOrWhiteSpace())); + if (variant is null) + { + continue; + } + + switch (schedule.Action) + { + case ContentScheduleAction.Release: + variant.ScheduledPublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero); + break; + case ContentScheduleAction.Expire: + variant.ScheduledUnPublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero); + break; + } + } + } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs index db1f2e67b663..8a7f154bc31f 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs @@ -7,4 +7,8 @@ public class DocumentVariantResponseModel : VariantResponseModelBase public DocumentVariantState State { get; set; } public DateTimeOffset? PublishDate { get; set; } + + public DateTimeOffset? ScheduledPublishDate { get; set; } + + public DateTimeOffset? ScheduledUnPublishDate { get; set; } } From 366d2ff24d89c8e3ce37c6488724baac9380cd41 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 7 Nov 2024 11:57:35 +0100 Subject: [PATCH 02/16] typo fix --- .../ViewModels/Document/DocumentVariantResponseModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs index 8a7f154bc31f..58b4768cde9d 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs @@ -10,5 +10,5 @@ public class DocumentVariantResponseModel : VariantResponseModelBase public DateTimeOffset? ScheduledPublishDate { get; set; } - public DateTimeOffset? ScheduledUnPublishDate { get; set; } + public DateTimeOffset? ScheduledUnpublishDate { get; set; } } From 4b457c0e55a5c1d15d29da3fa8c4c5bc01a92d1e Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 7 Nov 2024 11:59:46 +0100 Subject: [PATCH 03/16] stupid stuff --- .../Mapping/Document/DocumentMapDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs index bf1a859d17d4..00ac781f51fc 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs @@ -108,7 +108,7 @@ private void Map(ContentScheduleCollection source, DocumentResponseModel target, variant.ScheduledPublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero); break; case ContentScheduleAction.Expire: - variant.ScheduledUnPublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero); + variant.ScheduledUnpublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero); break; } } From 114d53db110a6c8c9dd98098b3a5bff4e27ffb48 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 14 Nov 2024 20:39:32 +0100 Subject: [PATCH 04/16] Enable content scheduling features in the publishing service --- .../Factories/DocumentPresentationFactory.cs | 6 +- .../Document/PublishDocumentRequestModel.cs | 1 - src/Umbraco.Core/Constants-System.cs | 2 + .../CultureAndScheduleModel.cs | 2 - .../ContentPublishing/CultureScheduleModel.cs | 21 ++++++ .../Models/ContentScheduleCollection.cs | 26 ++++++++ .../Services/ContentPublishingService.cs | 66 +++++++++++++++++-- src/Umbraco.Core/Services/ContentService.cs | 50 +++++++++++++- .../Services/IContentPublishingService.cs | 56 ++++++++++++++++ src/Umbraco.Core/Services/IContentService.cs | 1 + 10 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index f1e6e1aad9db..d26d90f62271 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -138,7 +138,7 @@ public Attempt Create { if (cultureAndScheduleRequestModel.Schedule is null || (cultureAndScheduleRequestModel.Schedule.PublishTime is null && cultureAndScheduleRequestModel.Schedule.UnpublishTime is null)) { - culturesToPublishImmediately.Add(cultureAndScheduleRequestModel.Culture ?? "*"); // API have `null` for invariant, but service layer has "*". + culturesToPublishImmediately.Add(cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture); // API have `null` for invariant, but service layer has "*". continue; } @@ -154,7 +154,7 @@ public Attempt Create } contentScheduleCollection.Add(new ContentSchedule( - cultureAndScheduleRequestModel.Culture ?? "*", + cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture, cultureAndScheduleRequestModel.Schedule.PublishTime.Value.UtcDateTime, ContentScheduleAction.Release)); } @@ -179,7 +179,7 @@ public Attempt Create } contentScheduleCollection.Add(new ContentSchedule( - cultureAndScheduleRequestModel.Culture ?? "*", + cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture, cultureAndScheduleRequestModel.Schedule.UnpublishTime.Value.UtcDateTime, ContentScheduleAction.Expire)); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs index 0bf1ba98be21..64cac6202e17 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs @@ -17,7 +17,6 @@ public class CultureAndScheduleRequestModel /// Gets or sets the schedule of publishing. Null means immediately. /// public ScheduleRequestModel? Schedule { get; set; } - } diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index a94a1cd58396..cf7c1de325fe 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -89,5 +89,7 @@ public static class System /// The DataDirectory placeholder. /// public const string DataDirectoryPlaceholder = "|DataDirectory|"; + + public const string InvariantCulture = "*"; } } diff --git a/src/Umbraco.Core/Models/ContentPublishing/CultureAndScheduleModel.cs b/src/Umbraco.Core/Models/ContentPublishing/CultureAndScheduleModel.cs index 0d4882ea854d..42a586508be9 100644 --- a/src/Umbraco.Core/Models/ContentPublishing/CultureAndScheduleModel.cs +++ b/src/Umbraco.Core/Models/ContentPublishing/CultureAndScheduleModel.cs @@ -5,5 +5,3 @@ public class CultureAndScheduleModel public required ISet CulturesToPublishImmediately { get; set; } public required ContentScheduleCollection Schedules { get; set; } } - - diff --git a/src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs b/src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs new file mode 100644 index 000000000000..b2f79f7d7a6a --- /dev/null +++ b/src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Cms.Core.Models.ContentPublishing; + +public class CulturePublishScheduleModel +{ + /// + /// Gets or sets the culture. Null means invariant. + /// + public string? Culture { get; set; } + + /// + /// Gets or sets the schedule of publishing. Null means immediately. + /// + public ScheduleModel? Schedule { get; set; } +} + +public class ScheduleModel +{ + public DateTimeOffset? PublishDate { get; set; } + + public DateTimeOffset? UnpublishDate { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index 4fb90779de5c..f2cccbb013dd 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -170,6 +170,32 @@ public void Remove(ContentSchedule change) } } + public void RemoveIfExists(string culture, ContentScheduleAction action) + { + ContentSchedule? changeToRemove = FullSchedule.FirstOrDefault(change => + change.Culture == culture + && change.Action == action); + if (changeToRemove is not null) + { + Remove(changeToRemove); + } + } + + public void AddOrUpdate(string culture, DateTime dateTime, ContentScheduleAction action) + { + // we need to remove the old one as ContentSchedule.Date is immutable + ContentSchedule? changeToRemove = FullSchedule.FirstOrDefault(change => + change.Culture == culture + && change.Action == action); + + if (changeToRemove is not null) + { + Remove(changeToRemove); + } + + Add(new ContentSchedule(culture, dateTime, action)); + } + /// /// Clear all of the scheduled change type for invariant content /// diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 24c73271eee5..6a6185157f61 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -34,6 +34,50 @@ public ContentPublishingService( } /// + public async Task> PublishAsync( + Guid key, + ICollection culturesToPublishOrSchedule, + Guid userKey) + { + var culturesToPublishImmediately = + culturesToPublishOrSchedule.Where(culture => culture.Schedule is null).Select(c => c.Culture ?? Constants.System.InvariantCulture).ToHashSet(); + + ContentScheduleCollection schedules = _contentService.GetContentScheduleByContentId(key); + + foreach (CulturePublishScheduleModel cultureToSchedule in culturesToPublishOrSchedule.Where(c => c.Schedule is not null)) + { + var culture = cultureToSchedule.Culture ?? Constants.System.InvariantCulture; + + if (cultureToSchedule.Schedule!.PublishDate is null) + { + schedules.RemoveIfExists(culture, ContentScheduleAction.Release); + } + else + { + schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.PublishDate.Value.UtcDateTime,ContentScheduleAction.Release); + } + + if (cultureToSchedule.Schedule!.UnpublishDate is null) + { + schedules.RemoveIfExists(culture, ContentScheduleAction.Expire); + } + else + { + schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.UnpublishDate.Value.UtcDateTime, ContentScheduleAction.Expire); + } + } + + var cultureAndSchedule = new CultureAndScheduleModel + { + CulturesToPublishImmediately = culturesToPublishImmediately, + Schedules = schedules, + }; + + return await PublishAsync(key, cultureAndSchedule, userKey); + } + + /// + [Obsolete("Use non obsoleted version instead. Scheduled for removal in v17")] public async Task> PublishAsync( Guid key, CultureAndScheduleModel cultureAndSchedule, @@ -47,6 +91,16 @@ public async Task x.IsoCode); - if (cultures.Any(x => x == "*") || cultures.All(x=> validCultures.Contains(x) is false)) + if (cultures.Any(x => x == "*") || validCultures.ContainsAll(cultures) is false) { scope.Complete(); return Attempt.FailWithStatus(ContentPublishingOperationStatus.InvalidCulture, new ContentPublishingResult()); @@ -95,10 +149,14 @@ public async Task? _queryNotTrashed; #region Constructors + public ContentService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDocumentRepository documentRepository, + IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, + Lazy propertyValidationService, + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver, + PropertyEditorCollection propertyEditorCollection, + IIdKeyMap idKeyMap) + : base(provider, loggerFactory, eventMessagesFactory) + { + _documentRepository = documentRepository; + _entityRepository = entityRepository; + _auditRepository = auditRepository; + _contentTypeRepository = contentTypeRepository; + _documentBlueprintRepository = documentBlueprintRepository; + _languageRepository = languageRepository; + _propertyValidationService = propertyValidationService; + _shortStringHelper = shortStringHelper; + _cultureImpactFactory = cultureImpactFactory; + _userIdKeyResolver = userIdKeyResolver; + _propertyEditorCollection = propertyEditorCollection; + _idKeyMap = idKeyMap; + _logger = loggerFactory.CreateLogger(); + } + + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V17.")] public ContentService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -68,6 +103,7 @@ public ContentService( _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; _propertyEditorCollection = propertyEditorCollection; + _idKeyMap = StaticServiceProvider.Instance.GetRequiredService(); _logger = loggerFactory.CreateLogger(); } @@ -100,7 +136,8 @@ public ContentService( shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService() + ) { } @@ -550,6 +587,17 @@ public ContentScheduleCollection GetContentScheduleByContentId(int contentId) } } + public ContentScheduleCollection GetContentScheduleByContentId(Guid contentId) + { + Attempt idAttempt = _idKeyMap.GetIdForKey(contentId, UmbracoObjectTypes.Document); + if (idAttempt.Success is false) + { + return new ContentScheduleCollection(); + } + + return GetContentScheduleByContentId(idAttempt.Result); + } + /// public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) { diff --git a/src/Umbraco.Core/Services/IContentPublishingService.cs b/src/Umbraco.Core/Services/IContentPublishingService.cs index 701aba31666e..598e9e5259b6 100644 --- a/src/Umbraco.Core/Services/IContentPublishingService.cs +++ b/src/Umbraco.Core/Services/IContentPublishingService.cs @@ -1,3 +1,6 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; using Umbraco.Cms.Core.Services.OperationStatus; @@ -12,6 +15,7 @@ public interface IContentPublishingService /// The cultures to publish and their publishing schedules. /// The identifier of the user performing the operation. /// Result of the publish operation. + [Obsolete("Use non obsoleted version instead. Scheduled for removal in v17")] Task> PublishAsync(Guid key, CultureAndScheduleModel cultureAndSchedule, Guid userKey); /// @@ -32,4 +36,56 @@ public interface IContentPublishingService /// The identifier of the user performing the operation. /// Status of the publish operation. Task> UnpublishAsync(Guid key, ISet? cultures, Guid userKey); + + /// + /// Publishes a single content item. + /// + /// The key of the root content. + /// The cultures to publish or schedule. + /// The identifier of the user performing the operation. + /// + async Task> PublishAsync( + Guid key, + ICollection culturesToPublishOrSchedule, + Guid userKey) + { + // todo remove default implementation when superseded method is removed in v17+ + var culturesToPublishImmediately = + culturesToPublishOrSchedule.Where(culture => culture.Schedule is null).Select(c => c.Culture ?? Constants.System.InvariantCulture).ToHashSet(); + + ContentScheduleCollection schedules = StaticServiceProvider.Instance.GetRequiredService().GetContentScheduleByContentId(key); + + foreach (CulturePublishScheduleModel cultureToSchedule in culturesToPublishOrSchedule.Where(c => c.Schedule is not null)) + { + var culture = cultureToSchedule.Culture ?? Constants.System.InvariantCulture; + + if (cultureToSchedule.Schedule!.PublishDate is null) + { + schedules.RemoveIfExists(culture, ContentScheduleAction.Release); + } + else + { + schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.PublishDate.Value.UtcDateTime,ContentScheduleAction.Release); + } + + if (cultureToSchedule.Schedule!.UnpublishDate is null) + { + schedules.RemoveIfExists(culture, ContentScheduleAction.Expire); + } + else + { + schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.UnpublishDate.Value.UtcDateTime, ContentScheduleAction.Expire); + } + } + + var cultureAndSchedule = new CultureAndScheduleModel + { + CulturesToPublishImmediately = culturesToPublishImmediately, + Schedules = schedules, + }; + +#pragma warning disable CS0618 // Type or member is obsolete + return await PublishAsync(key, cultureAndSchedule, userKey); +#pragma warning restore CS0618 // Type or member is obsolete + } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index a97f753f99d7..f3185e7d54ec 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -523,4 +523,5 @@ public interface IContentService : IContentServiceBase #endregion Task EmptyRecycleBinAsync(Guid userId); + ContentScheduleCollection GetContentScheduleByContentId(Guid contentId); } From dcf8e86447ac7b8bd4d9991232f895c22f3d6d97 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 14 Nov 2024 20:39:58 +0100 Subject: [PATCH 05/16] Replace obsoleted non async calls --- .../Umbraco.Core/Services/ContentEditingServiceTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentEditingServiceTests.cs index 75100f8cea2c..c5651486bc2a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentEditingServiceTests.cs @@ -28,6 +28,8 @@ public class ContentEditingServiceTests : UmbracoIntegrationTestWithContent private ILanguageService LanguageService => GetRequiredService(); + private ITemplateService TemplateService => GetRequiredService(); + [Test] public async Task Only_Supplied_Cultures_Are_Updated() { @@ -107,7 +109,7 @@ public async Task Only_Supplied_Cultures_Are_Updated() await LanguageService.CreateAsync(langDa, Constants.Security.SuperUserKey); var template = TemplateBuilder.CreateTextPageTemplate(); - FileService.SaveTemplate(template); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); var contentType = new ContentTypeBuilder() .WithAlias("variantContent") @@ -127,7 +129,7 @@ public async Task Only_Supplied_Cultures_Are_Updated() .Build(); contentType.AllowedAsRoot = true; - ContentTypeService.Save(contentType); + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); return (langEn, langDa, contentType); } From 7094855d593577968071282b775323f581f07d4e Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 14 Nov 2024 20:40:20 +0100 Subject: [PATCH 06/16] Add content scheduling test --- .../Services/ContentPublishingServiceTests.cs | 1307 +++++++++++++++++ 1 file changed, 1307 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs new file mode 100644 index 000000000000..5f9f8d9880e1 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs @@ -0,0 +1,1307 @@ +using Bogus.DataSets; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest( + Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true)] +public class ContentPublishingServiceTests : UmbracoIntegrationTestWithContent +{ + private const string UnknownCulture = "ke-Ke"; + + private readonly DateTime _schedulePublishDate = DateTime.UtcNow.AddDays(1); + private readonly DateTime _scheduleUnPublishDate = DateTime.UtcNow.AddDays(2); + + [SetUp] + public new void Setup() => ContentRepositoryBase.ThrowOnWarning = true; + + [TearDown] + public void Teardown() => ContentRepositoryBase.ThrowOnWarning = false; + + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + private ILanguageService LanguageService => GetRequiredService(); + + private ITemplateService TemplateService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + #region Publish + + [Test] + public async Task Can_Publish_Single_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List { new() { Culture = setupInfo.LangEn.IsoCode } }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(publishAttempt.Success); + var content = ContentService.GetById(setupData.Key); + Assert.AreEqual(1, content!.PublishedCultures.Count()); + } + + [Test] + public async Task Can_Publish_Some_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() { Culture = setupInfo.LangEn.IsoCode }, new() { Culture = setupInfo.LangDa.IsoCode }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(publishAttempt.Success); + var content = ContentService.GetById(setupData.Key); + Assert.AreEqual(2, content!.PublishedCultures.Count()); + } + + [Test] + public async Task Can_Publish_All_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() { Culture = setupInfo.LangEn.IsoCode }, + new() { Culture = setupInfo.LangDa.IsoCode }, + new() { Culture = setupInfo.LangBe.IsoCode }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(publishAttempt.Success); + var content = ContentService.GetById(setupData.Key); + Assert.AreEqual(3, content!.PublishedCultures.Count()); + } + + [Test] + public async Task Can_NOT_Publish_Invariant_In_Variant_Setup() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List { new() { Culture = Constants.System.InvariantCulture } }, + Constants.Security.SuperUserKey); + + Assert.IsFalse(publishAttempt.Success); + + var content = ContentService.GetById(setupData.Key); + Assert.AreEqual(0, content!.PublishedCultures.Count()); + } + + [Test] + public async Task Can_Publish_Invariant_In_Invariant_Setup() + { + var doctype = await SetupInvariantDoctypeAsync(); + var setupData = await CreateInvariantContentAsync(doctype); + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List { new() { Culture = Constants.System.InvariantCulture } }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(publishAttempt.Success); + + var content = ContentService.GetById(setupData.Key); + Assert.NotNull(content!.PublishDate); + } + + [Test] + public async Task Can_NOT_Publish_Unknown_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() { Culture = setupInfo.LangEn.IsoCode }, + new() { Culture = setupInfo.LangDa.IsoCode }, + new() { Culture = UnknownCulture }, + }, + Constants.Security.SuperUserKey); + + Assert.IsFalse(publishAttempt.Success); + + var content = ContentService.GetById(setupData.Key); + Assert.AreEqual(0, content!.PublishedCultures.Count()); + } + + [Test] + public async Task Can_NOT_Publish_Scheduled_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + } + }, + Constants.Security.SuperUserKey); + + if (scheduleAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List { new() { Culture = setupInfo.LangEn.IsoCode } }, + Constants.Security.SuperUserKey); + + Assert.IsFalse(publishAttempt.Success); + + var content = ContentService.GetById(setupData.Key); + Assert.AreEqual(0, content!.PublishedCultures.Count()); + } + + #endregion + + #region Schedule Publish + + [Test] + public async Task Can_Schedule_Publish_Single_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Single().Date); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Schedule_Publish_Some_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Single().Date); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Release).Single().Date); + Assert.AreEqual(2, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Schedule_Publish_All_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Single().Date); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Release).Single().Date); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangBe.IsoCode, ContentScheduleAction.Release).Single().Date); + Assert.AreEqual(3, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_NOT_Schedule_Publish_Unknown_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate } + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsFalse(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual(0, schedules.FullSchedule.Count); + }); + } + + #endregion + + #region Schedule Unpublish + + [Test] + public async Task Can_Schedule_Unpublish_Single_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Single().Date); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Schedule_Unpublish_Some_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Single().Date); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Expire).Single().Date); + Assert.AreEqual(2, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Schedule_Unpublish_All_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Single().Date); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Expire).Single().Date); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(setupInfo.LangBe.IsoCode, ContentScheduleAction.Expire).Single().Date); + Assert.AreEqual(3, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_NOT_Schedule_Unpublish_Unknown_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate } + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsFalse(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual(0, schedules.FullSchedule.Count); + }); + } + + #endregion + + #region Unschedule Publish + + [Test] + public async Task Can_Unschedule_Publish_Single_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Any()); + Assert.AreEqual(5, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Unschedule_Publish_Some_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate } + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate } + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Release).Any()); + Assert.AreEqual(4, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Unschedule_Publish_All_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Release).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangBe.IsoCode, ContentScheduleAction.Release).Any()); + Assert.AreEqual(3, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_NOT_Unschedule_Publish_Unknown_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsFalse(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual(6, schedules.FullSchedule.Count); + }); + } + + #endregion + + #region Unschedule Unpublish + + [Test] + public async Task Can_Unschedule_Unpublish_Single_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.AreEqual(5, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Unschedule_Unpublish_Some_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate } + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate } + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.AreEqual(4, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Unschedule_Unpublish_All_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangBe.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.AreEqual(3, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_NOT_Unschedule_Unpublish_Unknown_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsFalse(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual(6, schedules.FullSchedule.Count); + }); + } + + #endregion + + #region Clean Schedule + + [Test] + public async Task Can_Clear_Schedule_Single_Culture() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel(), + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.AreEqual(4, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Clear_Schedule_Some_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel(), + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel(), + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Release).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangEn.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Release).Any()); + Assert.IsFalse(schedules.GetSchedule(setupInfo.LangDa.IsoCode, ContentScheduleAction.Expire).Any()); + Assert.AreEqual(2, schedules.FullSchedule.Count); + }); + } + + [Test] + public async Task Can_Clear_Schedule_All_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishForAllCulturesAsync(setupData, setupInfo); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = new ScheduleModel(), + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel(), + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ScheduleModel(), + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.AreEqual(0, schedules.FullSchedule.Count); + }); + } + + #endregion + + #region Combinations + + + + #endregion + + #region Helper methods + + private async Task<(ILanguage LangEn, ILanguage LangDa, ILanguage LangBe, IContentType contentType)> + SetupVariantDoctypeAsync() + { + var langEn = (await LanguageService.GetAsync("en-US"))!; + var langDa = new LanguageBuilder() + .WithCultureInfo("da-DK") + .Build(); + await LanguageService.CreateAsync(langDa, Constants.Security.SuperUserKey); + var langBe = new LanguageBuilder() + .WithCultureInfo("nl-BE") + .Build(); + await LanguageService.CreateAsync(langBe, Constants.Security.SuperUserKey); + + var template = TemplateBuilder.CreateTextPageTemplate(); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + var contentType = new ContentTypeBuilder() + .WithAlias("variantContent") + .WithName("Variant Content") + .WithContentVariation(ContentVariation.Culture) + .AddPropertyGroup() + .WithAlias("content") + .WithName("Content") + .WithSupportsPublishing(true) + .Done() + .Build(); + + contentType.AllowedAsRoot = true; + var createAttempt = await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + if (createAttempt.Success is false) + { + throw new Exception("Something unexpected went wrong setting up the test data structure"); + } + + return (langEn, langDa, langBe, contentType); + } + + private async Task CreateVariantContentAsync(ILanguage langEn, ILanguage langDa, ILanguage langBe, + IContentType contentType) + { + var documentKey = Guid.NewGuid(); + + var createModel = new ContentCreateModel + { + Key = documentKey, + ContentTypeKey = contentType.Key, + Variants = new[] + { + new VariantModel + { + Name = langEn.CultureName, + Culture = langEn.IsoCode, + Properties = Enumerable.Empty(), + }, + new VariantModel + { + Name = langDa.CultureName, + Culture = langDa.IsoCode, + Properties = Enumerable.Empty(), + }, + new VariantModel + { + Name = langBe.CultureName, + Culture = langBe.IsoCode, + Properties = Enumerable.Empty(), + } + } + }; + + var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + if (createAttempt.Success is false) + { + throw new Exception("Something unexpected went wrong setting up the test data"); + } + + return createAttempt.Result.Content!; + } + + private async Task SetupInvariantDoctypeAsync() + { + var template = TemplateBuilder.CreateTextPageTemplate(); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + var contentType = new ContentTypeBuilder() + .WithAlias("invariantContent") + .WithName("Invariant Content") + .AddPropertyGroup() + .WithAlias("content") + .WithName("Content") + .WithSupportsPublishing(true) + .Done() + .Build(); + + contentType.AllowedAsRoot = true; + var createAttempt = await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + if (createAttempt.Success is false) + { + throw new Exception("Something unexpected went wrong setting up the test data structure"); + } + + return contentType; + } + + private async Task CreateInvariantContentAsync(IContentType contentType) + { + var documentKey = Guid.NewGuid(); + + var createModel = new ContentCreateModel + { + Key = documentKey, ContentTypeKey = contentType.Key, InvariantName = "Test", + }; + + var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + if (createAttempt.Success is false) + { + throw new Exception("Something unexpected went wrong setting up the test data"); + } + + return createAttempt.Result.Content!; + } + + private async Task> + SchedulePublishAndUnPublishForAllCulturesAsync( + IContent setupData, + (ILanguage LangEn, ILanguage LangDa, ILanguage LangBe, IContentType contentType) setupInfo) + => await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + Schedule = + new ScheduleModel + { + PublishDate = _schedulePublishDate, UnpublishDate = _scheduleUnPublishDate, + }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = + new ScheduleModel + { + PublishDate = _schedulePublishDate, UnpublishDate = _scheduleUnPublishDate, + }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ScheduleModel + { + PublishDate = _schedulePublishDate, UnpublishDate = _scheduleUnPublishDate, + }, + }, + }, + Constants.Security.SuperUserKey); + + #endregion +} From c05aaebf4b9874dd4d30e7e774721c6f34efeacd Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Fri, 15 Nov 2024 09:23:06 +0100 Subject: [PATCH 07/16] Publush and schedule combination test --- .../Services/ContentPublishingServiceTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs index 5f9f8d9880e1..4398bef46535 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs @@ -1139,7 +1139,43 @@ public async Task Can_Clear_Schedule_All_Cultures() #region Combinations + [Test] + public async Task Can_Publish_And_Schedule_Different_Cultures() + { + var setupInfo = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync( + setupInfo.LangEn, + setupInfo.LangDa, + setupInfo.LangBe, + setupInfo.contentType); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = setupInfo.LangEn.IsoCode, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(1, content!.PublishedCultures.Count()); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } #endregion From 73b9786bd9eba7b5cc344a569a8346156e4e64e1 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Mon, 18 Nov 2024 10:40:40 +0100 Subject: [PATCH 08/16] More invariantCulture notation allignment and more tests --- .../ContentPublishing/CultureScheduleModel.cs | 4 +- .../Services/ContentPublishingService.cs | 2 +- .../Implement/DocumentRepository.cs | 2 +- .../Services/ContentPublishingServiceTests.cs | 305 +++++++++++++++--- 4 files changed, 261 insertions(+), 52 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs b/src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs index b2f79f7d7a6a..5b00bfc798f2 100644 --- a/src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs +++ b/src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs @@ -10,10 +10,10 @@ public class CulturePublishScheduleModel /// /// Gets or sets the schedule of publishing. Null means immediately. /// - public ScheduleModel? Schedule { get; set; } + public ContentScheduleModel? Schedule { get; set; } } -public class ScheduleModel +public class ContentScheduleModel { public DateTimeOffset? PublishDate { get; set; } diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 6a6185157f61..dad06bd0c7f1 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -123,7 +123,7 @@ public async Task x != "*")) + if (cultures.Length != 1 || cultures.Any(x => x != Constants.System.InvariantCulture)) { scope.Complete(); return Attempt.FailWithStatus(ContentPublishingOperationStatus.InvalidCulture, new ContentPublishingResult()); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 5c11e512f1ba..24bc09a33a7c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -117,7 +117,7 @@ public ContentScheduleCollection GetContentSchedule(int contentId) { result.Add(new ContentSchedule( scheduleDto.Id, - LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? Constants.System.InvariantCulture, scheduleDto.Date, scheduleDto.Action == ContentScheduleAction.Release.ToString() ? ContentScheduleAction.Release diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs index 4398bef46535..4dfa43778cb9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs @@ -147,6 +147,8 @@ public async Task Can_Publish_Invariant_In_Invariant_Setup() var content = ContentService.GetById(setupData.Key); Assert.NotNull(content!.PublishDate); } + //todo more tests for invariant + //todo update schedule date [Test] public async Task Can_NOT_Publish_Unknown_Culture() @@ -191,7 +193,7 @@ public async Task Can_NOT_Publish_Scheduled_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, } }, Constants.Security.SuperUserKey); @@ -216,6 +218,40 @@ public async Task Can_NOT_Publish_Scheduled_Culture() #region Schedule Publish + [Test] + public async Task Can_Schedule_Publish_Invariant() + { + var doctype = await SetupInvariantDoctypeAsync(); + var setupData = await CreateInvariantContentAsync(doctype); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = Constants.System.InvariantCulture, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsNull(content!.PublishDate); + Assert.AreEqual( + _schedulePublishDate, + schedules.GetSchedule(Constants.System.InvariantCulture, ContentScheduleAction.Release).Single().Date); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } + [Test] public async Task Can_Schedule_Publish_Single_Culture() { @@ -233,7 +269,7 @@ public async Task Can_Schedule_Publish_Single_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -270,12 +306,12 @@ public async Task Can_Schedule_Publish_Some_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -315,17 +351,17 @@ public async Task Can_Schedule_Publish_All_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangBe.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -368,17 +404,17 @@ public async Task Can_NOT_Schedule_Publish_Unknown_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = UnknownCulture, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate } + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate } }, }, Constants.Security.SuperUserKey); @@ -399,6 +435,40 @@ public async Task Can_NOT_Schedule_Publish_Unknown_Culture() #region Schedule Unpublish + [Test] + public async Task Can_Schedule_Unpublish_Invariant() + { + var doctype = await SetupInvariantDoctypeAsync(); + var setupData = await CreateInvariantContentAsync(doctype); + + var scheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = Constants.System.InvariantCulture, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(scheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsNull(content!.PublishDate); + Assert.AreEqual( + _scheduleUnPublishDate, + schedules.GetSchedule(Constants.System.InvariantCulture, ContentScheduleAction.Expire).Single().Date); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } + [Test] public async Task Can_Schedule_Unpublish_Single_Culture() { @@ -416,7 +486,7 @@ public async Task Can_Schedule_Unpublish_Single_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -453,12 +523,12 @@ public async Task Can_Schedule_Unpublish_Some_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -498,17 +568,17 @@ public async Task Can_Schedule_Unpublish_All_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangBe.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -551,17 +621,17 @@ public async Task Can_NOT_Schedule_Unpublish_Unknown_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, }, new() { Culture = UnknownCulture, - Schedule = new ScheduleModel { UnpublishDate = _schedulePublishDate } + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate } }, }, Constants.Security.SuperUserKey); @@ -582,6 +652,47 @@ public async Task Can_NOT_Schedule_Unpublish_Unknown_Culture() #region Unschedule Publish + [Test] + public async Task Can_UnSchedule_Publish_Invariant() + { + var doctype = await SetupInvariantDoctypeAsync(); + var setupData = await CreateInvariantContentAsync(doctype); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishInvariantAsync(setupData); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var unscheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = Constants.System.InvariantCulture, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(unscheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsNull(content!.PublishDate); + Assert.IsFalse(schedules.GetSchedule(Constants.System.InvariantCulture, ContentScheduleAction.Release).Any()); + Assert.IsTrue(schedules.GetSchedule(Constants.System.InvariantCulture, ContentScheduleAction.Expire).Any()); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } + [Test] public async Task Can_Unschedule_Publish_Single_Culture() { @@ -607,7 +718,7 @@ public async Task Can_Unschedule_Publish_Single_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, }, }, Constants.Security.SuperUserKey); @@ -650,12 +761,12 @@ public async Task Can_Unschedule_Publish_Some_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate } + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate } }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate } + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate } }, }, Constants.Security.SuperUserKey); @@ -699,17 +810,17 @@ public async Task Can_Unschedule_Publish_All_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, }, new() { Culture = setupInfo.LangBe.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, }, }, Constants.Security.SuperUserKey); @@ -754,17 +865,17 @@ public async Task Can_NOT_Unschedule_Publish_Unknown_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, }, new() { Culture = UnknownCulture, - Schedule = new ScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, }, }, Constants.Security.SuperUserKey); @@ -785,6 +896,47 @@ public async Task Can_NOT_Unschedule_Publish_Unknown_Culture() #region Unschedule Unpublish + [Test] + public async Task Can_UnSchedule_Unpublish_Invariant() + { + var doctype = await SetupInvariantDoctypeAsync(); + var setupData = await CreateInvariantContentAsync(doctype); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishInvariantAsync(setupData); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var unscheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = Constants.System.InvariantCulture, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(unscheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsNull(content!.PublishDate); + Assert.IsFalse(schedules.GetSchedule(Constants.System.InvariantCulture, ContentScheduleAction.Expire).Any()); + Assert.IsTrue(schedules.GetSchedule(Constants.System.InvariantCulture, ContentScheduleAction.Release).Any()); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } + [Test] public async Task Can_Unschedule_Unpublish_Single_Culture() { @@ -810,7 +962,7 @@ public async Task Can_Unschedule_Unpublish_Single_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -853,12 +1005,12 @@ public async Task Can_Unschedule_Unpublish_Some_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate } + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate } }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate } + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate } }, }, Constants.Security.SuperUserKey); @@ -902,17 +1054,17 @@ public async Task Can_Unschedule_Unpublish_All_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangBe.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -957,17 +1109,17 @@ public async Task Can_NOT_Unschedule_Unpublish_Unknown_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, new() { Culture = UnknownCulture, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -988,6 +1140,45 @@ public async Task Can_NOT_Unschedule_Unpublish_Unknown_Culture() #region Clean Schedule + [Test] + public async Task Can_Clear_Schedule_Invariant() + { + var doctype = await SetupInvariantDoctypeAsync(); + var setupData = await CreateInvariantContentAsync(doctype); + + var scheduleSetupAttempt = + await SchedulePublishAndUnPublishInvariantAsync(setupData); + + if (scheduleSetupAttempt.Success is false) + { + throw new Exception("Setup failed"); + } + + var clearScheduleAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = Constants.System.InvariantCulture, + Schedule = new ContentScheduleModel(), + }, + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(clearScheduleAttempt.Success); + + var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); + var content = ContentService.GetById(setupData.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(0, content!.PublishedCultures.Count()); + Assert.IsNull(content!.PublishDate); + Assert.AreEqual(0, schedules.FullSchedule.Count); + }); + } + [Test] public async Task Can_Clear_Schedule_Single_Culture() { @@ -1013,7 +1204,7 @@ public async Task Can_Clear_Schedule_Single_Culture() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel(), + Schedule = new ContentScheduleModel(), }, }, Constants.Security.SuperUserKey); @@ -1057,12 +1248,12 @@ public async Task Can_Clear_Schedule_Some_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel(), + Schedule = new ContentScheduleModel(), }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel(), + Schedule = new ContentScheduleModel(), }, }, Constants.Security.SuperUserKey); @@ -1108,17 +1299,17 @@ public async Task Can_Clear_Schedule_All_Cultures() new() { Culture = setupInfo.LangEn.IsoCode, - Schedule = new ScheduleModel(), + Schedule = new ContentScheduleModel(), }, new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel(), + Schedule = new ContentScheduleModel(), }, new() { Culture = setupInfo.LangBe.IsoCode, - Schedule = new ScheduleModel(), + Schedule = new ContentScheduleModel(), }, }, Constants.Security.SuperUserKey); @@ -1160,7 +1351,7 @@ public async Task Can_Publish_And_Schedule_Different_Cultures() new() { Culture = setupInfo.LangDa.IsoCode, - Schedule = new ScheduleModel { PublishDate = _schedulePublishDate }, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, }, }, Constants.Security.SuperUserKey); @@ -1176,7 +1367,6 @@ public async Task Can_Publish_And_Schedule_Different_Cultures() Assert.AreEqual(1, schedules.FullSchedule.Count); }); } - #endregion #region Helper methods @@ -1314,7 +1504,7 @@ private async Task> + SchedulePublishAndUnPublishInvariantAsync( + IContent setupData) + => await ContentPublishingService.PublishAsync( + setupData.Key, + new List + { + new() + { + Culture = Constants.System.InvariantCulture, + Schedule = + new ContentScheduleModel + { + PublishDate = _schedulePublishDate, UnpublishDate = _scheduleUnPublishDate, + }, + }, + }, + Constants.Security.SuperUserKey); + #endregion } From dd0bd781138aae1194f181dfbba765dd37efa99b Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Mon, 18 Nov 2024 10:40:53 +0100 Subject: [PATCH 09/16] Link up api with updated document scheduling --- .../Document/PublishDocumentController.cs | 4 +- .../Factories/DocumentPresentationFactory.cs | 50 +++++++++++++++++ .../Factories/IDocumentPresentationFactory.cs | 54 ++++++++++++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs index 3c39270a3e91..fe29aa77cd34 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs @@ -46,7 +46,7 @@ public async Task Publish(CancellationToken cancellationToken, Gu { AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( User, - ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x=>x.Culture is not null).Select(x=>x.Culture!)), + ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x => x.Culture is not null).Select(x=>x.Culture!)), AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) @@ -54,7 +54,7 @@ public async Task Publish(CancellationToken cancellationToken, Gu return Forbidden(); } - Attempt modelResult = _documentPresentationFactory.CreateCultureAndScheduleModel(requestModel); + Attempt, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCulturePublishScheduleModels(requestModel); if (modelResult.Success is false) { diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index d26d90f62271..522435ba13ea 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -130,6 +130,7 @@ public IEnumerable CreateVariantsItemResponseM public DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IDocumentEntitySlim entity) => _umbracoMapper.Map(entity)!; + [Obsolete("Use CreateCulturePublishScheduleModels instead. Scheduled for removal in v17")] public Attempt CreateCultureAndScheduleModel(PublishDocumentRequestModel requestModel) { var contentScheduleCollection = new ContentScheduleCollection(); @@ -190,4 +191,53 @@ public Attempt Create CulturesToPublishImmediately = culturesToPublishImmediately, }); } + + // NOTE: Keep the default implementation on the interface in line with this one until + // the default implementation can be removed + public Attempt, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels(PublishDocumentRequestModel requestModel) + { + var model = new List(); + + foreach (CultureAndScheduleRequestModel cultureAndScheduleRequestModel in requestModel.PublishSchedules) + { + if (cultureAndScheduleRequestModel.Schedule is null) + { + model.Add(new CulturePublishScheduleModel + { + Culture = cultureAndScheduleRequestModel.Culture + ?? Constants.System.InvariantCulture // API have `null` for invariant, but service layer has "*". + }); + continue; + } + + if (cultureAndScheduleRequestModel.Schedule.PublishTime is not null + && cultureAndScheduleRequestModel.Schedule.PublishTime <= _timeProvider.GetUtcNow()) + { + return Attempt.FailWithStatus(ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture, model); + } + + if (cultureAndScheduleRequestModel.Schedule.UnpublishTime is not null + && cultureAndScheduleRequestModel.Schedule.UnpublishTime <= _timeProvider.GetUtcNow()) + { + return Attempt.FailWithStatus(ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture, model); + } + + if (cultureAndScheduleRequestModel.Schedule.UnpublishTime <= cultureAndScheduleRequestModel.Schedule.PublishTime) + { + return Attempt.FailWithStatus(ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime, model); + } + + model.Add(new CulturePublishScheduleModel + { + Culture = cultureAndScheduleRequestModel.Culture, + Schedule = new ContentScheduleModel + { + PublishDate = cultureAndScheduleRequestModel.Schedule.PublishTime, + UnpublishDate = cultureAndScheduleRequestModel.Schedule.UnpublishTime, + }, + }); + } + + return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model); + } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs index 51643a0b7225..0d4f2443339f 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs @@ -1,8 +1,10 @@ -using Umbraco.Cms.Api.Management.ViewModels.Document; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Item; using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint.Item; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; using Umbraco.Cms.Core.Models.Entities; @@ -29,5 +31,55 @@ Task CreateResponseModelAsync(IContent content, ContentSc DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IDocumentEntitySlim entity); + [Obsolete("Use CreateCulturePublishScheduleModels instead. Scheduled for removal in v17")] Attempt CreateCultureAndScheduleModel(PublishDocumentRequestModel requestModel); + + Attempt, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels( + PublishDocumentRequestModel requestModel) + { + // todo remove default implementation when obsolete method is removed + var model = new List(); + + foreach (CultureAndScheduleRequestModel cultureAndScheduleRequestModel in requestModel.PublishSchedules) + { + if (cultureAndScheduleRequestModel.Schedule is null) + { + model.Add(new CulturePublishScheduleModel + { + Culture = cultureAndScheduleRequestModel.Culture + ?? Constants.System.InvariantCulture + }); + continue; + } + + if (cultureAndScheduleRequestModel.Schedule.PublishTime is not null + && cultureAndScheduleRequestModel.Schedule.PublishTime <= StaticServiceProvider.Instance.GetRequiredService().GetUtcNow()) + { + return Attempt.FailWithStatus(ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture, model); + } + + if (cultureAndScheduleRequestModel.Schedule.UnpublishTime is not null + && cultureAndScheduleRequestModel.Schedule.UnpublishTime <= StaticServiceProvider.Instance.GetRequiredService().GetUtcNow()) + { + return Attempt.FailWithStatus(ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture, model); + } + + if (cultureAndScheduleRequestModel.Schedule.UnpublishTime <= cultureAndScheduleRequestModel.Schedule.PublishTime) + { + return Attempt.FailWithStatus(ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime, model); + } + + model.Add(new CulturePublishScheduleModel + { + Culture = cultureAndScheduleRequestModel.Culture, + Schedule = new ContentScheduleModel + { + PublishDate = cultureAndScheduleRequestModel.Schedule.PublishTime, + UnpublishDate = cultureAndScheduleRequestModel.Schedule.UnpublishTime, + }, + }); + } + + return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model); + } } From e98f25b3a333b636efe2813bbbc7457dd084b8d3 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Mon, 18 Nov 2024 13:56:31 +0100 Subject: [PATCH 10/16] More invariant culture notation allignment --- .../Factories/DocumentPresentationFactory.cs | 2 +- .../Factories/IDocumentPresentationFactory.cs | 2 +- src/Umbraco.Core/Extensions/ContentExtensions.cs | 6 +++--- src/Umbraco.Core/Models/ContentScheduleCollection.cs | 8 ++++---- src/Umbraco.Core/Services/ContentService.cs | 4 ++-- .../Umbraco.Core/Services/ContentServiceTests.cs | 2 +- .../Umbraco.Core/Models/ContentScheduleTests.cs | 5 +++-- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index 49155f44e050..e6b5161fe03d 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -71,7 +71,7 @@ public async Task CreatePublishedResponseModelAs return responseModel; } - + public async Task CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule) { DocumentResponseModel responseModel = _umbracoMapper.Map(content)!; diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs index 50749481477e..ae725e97a903 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs @@ -18,7 +18,7 @@ public interface IDocumentPresentationFactory Task CreateResponseModelAsync(IContent content); Task CreatePublishedResponseModelAsync(IContent content); - + Task CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule) #pragma warning disable CS0618 // Type or member is obsolete // Remove when obsolete CreateResponseModelAsync is removed diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index 7d0bba26f817..dde4ffb397a0 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -237,20 +237,20 @@ public static ContentStatus GetStatus(this IContent content, ContentScheduleColl if (!content.ContentType.VariesByCulture()) { - culture = string.Empty; + culture = Constants.System.InvariantCulture; } else if (culture.IsNullOrWhiteSpace()) { throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); } - IEnumerable expires = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Expire); + IEnumerable expires = contentSchedule.GetSchedule(culture, ContentScheduleAction.Expire); if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) { return ContentStatus.Expired; } - IEnumerable release = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Release); + IEnumerable release = contentSchedule.GetSchedule(culture, ContentScheduleAction.Release); if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) { return ContentStatus.AwaitingRelease; diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index f2cccbb013dd..8dbdd76e795e 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -64,7 +64,7 @@ public bool Equals(ContentScheduleCollection? other) public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) { var schedule = new ContentScheduleCollection(); - schedule.Add(string.Empty, release, expire); + schedule.Add(Constants.System.InvariantCulture, release, expire); return schedule; } @@ -98,7 +98,7 @@ public void Add(ContentSchedule schedule) /// /// /// - public bool Add(DateTime? releaseDate, DateTime? expireDate) => Add(string.Empty, releaseDate, expireDate); + public bool Add(DateTime? releaseDate, DateTime? expireDate) => Add(Constants.System.InvariantCulture, releaseDate, expireDate); /// /// Adds a new schedule for a culture @@ -202,7 +202,7 @@ public void AddOrUpdate(string culture, DateTime dateTime, ContentScheduleAction /// /// If specified, will clear all entries with dates less than or equal to the value public void Clear(ContentScheduleAction action, DateTime? changeDate = null) => - Clear(string.Empty, action, changeDate); + Clear(Constants.System.InvariantCulture, action, changeDate); /// /// Clear all of the scheduled change type for the culture @@ -252,7 +252,7 @@ public IReadOnlyList GetPending(ContentScheduleAction action, D /// /// public IEnumerable GetSchedule(ContentScheduleAction? action = null) => - GetSchedule(string.Empty, action); + GetSchedule(Constants.System.InvariantCulture, action); /// /// Gets the schedule for a culture diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 98e05963a4c3..e884cb303f23 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -3221,8 +3221,8 @@ private PublishResult StrategyCanPublish( ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id); - // loop over each culture publishing - or string.Empty for invariant - foreach (var culture in culturesPublishing ?? new[] { string.Empty }) + // loop over each culture publishing - or InvariantCulture for invariant + foreach (var culture in culturesPublishing ?? new[] { Constants.System.InvariantCulture }) { // ensure that the document status is correct // note: culture will be string.Empty for invariant diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 352400c4dd72..625068adaec8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -328,7 +328,7 @@ public void Remove_Scheduled_Publishing_Date() contentSchedule = ContentService.GetContentScheduleByContentId(content.Id); var sched = contentSchedule.FullSchedule; Assert.AreEqual(1, sched.Count); - Assert.AreEqual(1, sched.Count(x => x.Culture == string.Empty)); + Assert.AreEqual(1, sched.Count(x => x.Culture == Constants.System.InvariantCulture)); contentSchedule.Clear(ContentScheduleAction.Expire); ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentScheduleTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentScheduleTests.cs index 78bfa0a3f87f..a6958ae9309c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentScheduleTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentScheduleTests.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models; @@ -44,7 +45,7 @@ public void Can_Remove_Invariant() var now = DateTime.Now; var schedule = new ContentScheduleCollection(); schedule.Add(now, null); - var invariantSched = schedule.GetSchedule(string.Empty); + var invariantSched = schedule.GetSchedule(Constants.System.InvariantCulture); schedule.Remove(invariantSched.First()); Assert.AreEqual(0, schedule.FullSchedule.Count()); } @@ -56,7 +57,7 @@ public void Can_Remove_Variant() var schedule = new ContentScheduleCollection(); schedule.Add(now, null); schedule.Add("en-US", now, null); - var invariantSched = schedule.GetSchedule(string.Empty); + var invariantSched = schedule.GetSchedule(Constants.System.InvariantCulture); schedule.Remove(invariantSched.First()); Assert.AreEqual(0, schedule.GetSchedule(string.Empty).Count()); Assert.AreEqual(1, schedule.FullSchedule.Count()); From 2b93255e9ada92dfa9e1186ad597504c61aa7b8c Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Mon, 18 Nov 2024 14:14:13 +0100 Subject: [PATCH 11/16] Fix breaking change --- src/Umbraco.Core/Services/ContentService.cs | 1 - src/Umbraco.Core/Services/IContentService.cs | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e884cb303f23..dde3300814c6 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -13,7 +13,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; -using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index f3185e7d54ec..1f12a3df7e68 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; @@ -523,5 +525,16 @@ public interface IContentService : IContentServiceBase #endregion Task EmptyRecycleBinAsync(Guid userId); - ContentScheduleCollection GetContentScheduleByContentId(Guid contentId); + + ContentScheduleCollection GetContentScheduleByContentId(Guid contentId) + { + // Todo clean up default implementation in v17 + Attempt idAttempt = StaticServiceProvider.Instance.GetRequiredService() + .GetIdForKey(contentId, UmbracoObjectTypes.Document); + if (idAttempt.Success is false) + { + throw new ArgumentException("Invalid contentId"); + } + return GetContentScheduleByContentId(idAttempt.Result); + } } From 75079b25830052107ca63aa74d992cadeba07950 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 25 Nov 2024 09:18:20 +0100 Subject: [PATCH 12/16] Return expected status codes. --- src/Umbraco.Core/Services/ContentPublishingService.cs | 8 +++++++- .../Services/ContentPublishingServiceTests.cs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 8299e8bf1a4f..f59b0f2482cc 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -117,8 +117,14 @@ public async Task x == Constants.System.InvariantCulture)) + { + scope.Complete(); + return Attempt.FailWithStatus(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant, new ContentPublishingResult()); + } + var validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode); - if (cultures.Any(x => x == "*") || validCultures.ContainsAll(cultures) is false) + if (validCultures.ContainsAll(cultures) is false) { scope.Complete(); return Attempt.FailWithStatus(ContentPublishingOperationStatus.InvalidCulture, new ContentPublishingResult()); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs index 4dfa43778cb9..0853bca8c56a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs @@ -126,6 +126,7 @@ public async Task Can_NOT_Publish_Invariant_In_Variant_Setup() Constants.Security.SuperUserKey); Assert.IsFalse(publishAttempt.Success); + Assert.AreEqual(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant, publishAttempt.Status); var content = ContentService.GetById(setupData.Key); Assert.AreEqual(0, content!.PublishedCultures.Count()); @@ -171,6 +172,7 @@ public async Task Can_NOT_Publish_Unknown_Culture() Constants.Security.SuperUserKey); Assert.IsFalse(publishAttempt.Success); + Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, publishAttempt.Status); var content = ContentService.GetById(setupData.Key); Assert.AreEqual(0, content!.PublishedCultures.Count()); @@ -209,6 +211,7 @@ public async Task Can_NOT_Publish_Scheduled_Culture() Constants.Security.SuperUserKey); Assert.IsFalse(publishAttempt.Success); + Assert.AreEqual(ContentPublishingOperationStatus.CultureAwaitingRelease, publishAttempt.Status); var content = ContentService.GetById(setupData.Key); Assert.AreEqual(0, content!.PublishedCultures.Count()); @@ -420,6 +423,7 @@ public async Task Can_NOT_Schedule_Publish_Unknown_Culture() Constants.Security.SuperUserKey); Assert.IsFalse(scheduleAttempt.Success); + Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, scheduleAttempt.Status); var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); var content = ContentService.GetById(setupData.Key); @@ -637,6 +641,7 @@ public async Task Can_NOT_Schedule_Unpublish_Unknown_Culture() Constants.Security.SuperUserKey); Assert.IsFalse(scheduleAttempt.Success); + Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, scheduleAttempt.Status); var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); var content = ContentService.GetById(setupData.Key); @@ -881,6 +886,7 @@ public async Task Can_NOT_Unschedule_Publish_Unknown_Culture() Constants.Security.SuperUserKey); Assert.IsFalse(scheduleAttempt.Success); + Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, scheduleAttempt.Status); var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); var content = ContentService.GetById(setupData.Key); @@ -1125,7 +1131,7 @@ public async Task Can_NOT_Unschedule_Unpublish_Unknown_Culture() Constants.Security.SuperUserKey); Assert.IsFalse(scheduleAttempt.Success); - + Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, scheduleAttempt.Status); var schedules = ContentService.GetContentScheduleByContentId(setupData.Id); var content = ContentService.GetById(setupData.Key); From afabd09d9cfc493475c3d1bc2b5084198f9f941b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 25 Nov 2024 14:50:56 +0100 Subject: [PATCH 13/16] Fix constructor --- src/Umbraco.Core/Services/ContentService.cs | 30 +++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 74919e451401..3cf1d86ce97a 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -89,21 +89,23 @@ public ContentService( ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, PropertyEditorCollection propertyEditorCollection) - : base(provider, loggerFactory, eventMessagesFactory) + : this( + provider, + loggerFactory, + eventMessagesFactory, + documentRepository, + entityRepository, + auditRepository, + contentTypeRepository, + documentBlueprintRepository, + languageRepository, + propertyValidationService, + shortStringHelper, + cultureImpactFactory, + userIdKeyResolver, + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) { - _documentRepository = documentRepository; - _entityRepository = entityRepository; - _auditRepository = auditRepository; - _contentTypeRepository = contentTypeRepository; - _documentBlueprintRepository = documentBlueprintRepository; - _languageRepository = languageRepository; - _propertyValidationService = propertyValidationService; - _shortStringHelper = shortStringHelper; - _cultureImpactFactory = cultureImpactFactory; - _userIdKeyResolver = userIdKeyResolver; - _propertyEditorCollection = propertyEditorCollection; - _idKeyMap = StaticServiceProvider.Instance.GetRequiredService(); - _logger = loggerFactory.CreateLogger(); } [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] From 14adafffbe7acd387b14eda134f5709cefa92187 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Tue, 26 Nov 2024 14:33:49 +0100 Subject: [PATCH 14/16] Forward Default implementation to actual core implementation Co-authored-by: Bjarke Berg --- .../Services/IContentPublishingService.cs | 45 ++----------------- 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Core/Services/IContentPublishingService.cs b/src/Umbraco.Core/Services/IContentPublishingService.cs index 598e9e5259b6..73fc6685435e 100644 --- a/src/Umbraco.Core/Services/IContentPublishingService.cs +++ b/src/Umbraco.Core/Services/IContentPublishingService.cs @@ -44,48 +44,9 @@ public interface IContentPublishingService /// The cultures to publish or schedule. /// The identifier of the user performing the operation. /// - async Task> PublishAsync( + Task> PublishAsync( Guid key, ICollection culturesToPublishOrSchedule, - Guid userKey) - { - // todo remove default implementation when superseded method is removed in v17+ - var culturesToPublishImmediately = - culturesToPublishOrSchedule.Where(culture => culture.Schedule is null).Select(c => c.Culture ?? Constants.System.InvariantCulture).ToHashSet(); - - ContentScheduleCollection schedules = StaticServiceProvider.Instance.GetRequiredService().GetContentScheduleByContentId(key); - - foreach (CulturePublishScheduleModel cultureToSchedule in culturesToPublishOrSchedule.Where(c => c.Schedule is not null)) - { - var culture = cultureToSchedule.Culture ?? Constants.System.InvariantCulture; - - if (cultureToSchedule.Schedule!.PublishDate is null) - { - schedules.RemoveIfExists(culture, ContentScheduleAction.Release); - } - else - { - schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.PublishDate.Value.UtcDateTime,ContentScheduleAction.Release); - } - - if (cultureToSchedule.Schedule!.UnpublishDate is null) - { - schedules.RemoveIfExists(culture, ContentScheduleAction.Expire); - } - else - { - schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.UnpublishDate.Value.UtcDateTime, ContentScheduleAction.Expire); - } - } - - var cultureAndSchedule = new CultureAndScheduleModel - { - CulturesToPublishImmediately = culturesToPublishImmediately, - Schedules = schedules, - }; - -#pragma warning disable CS0618 // Type or member is obsolete - return await PublishAsync(key, cultureAndSchedule, userKey); -#pragma warning restore CS0618 // Type or member is obsolete - } + Guid userKey) => StaticServiceProvider.Instance.GetRequiredService() + .PublishAsync(key, culturesToPublishOrSchedule, userKey); } From 32faaeb6aaf78e7ce8bf0276f92beaf7fdc464ed Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Tue, 26 Nov 2024 14:34:37 +0100 Subject: [PATCH 15/16] Forward default implementation to core implementation Co-authored-by: Bjarke Berg --- src/Umbraco.Core/Services/IContentService.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 1f12a3df7e68..fd304f8107e4 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -526,15 +526,6 @@ public interface IContentService : IContentServiceBase Task EmptyRecycleBinAsync(Guid userId); - ContentScheduleCollection GetContentScheduleByContentId(Guid contentId) - { - // Todo clean up default implementation in v17 - Attempt idAttempt = StaticServiceProvider.Instance.GetRequiredService() - .GetIdForKey(contentId, UmbracoObjectTypes.Document); - if (idAttempt.Success is false) - { - throw new ArgumentException("Invalid contentId"); - } - return GetContentScheduleByContentId(idAttempt.Result); - } +ContentScheduleCollection GetContentScheduleByContentId(Guid contentId) => StaticServiceProvider.Instance + .GetRequiredService().GetContentScheduleByContentId(contentId); } From 1320eae162f7469d2cf084f6aec9c61859327177 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Tue, 26 Nov 2024 14:32:20 +0100 Subject: [PATCH 16/16] Make content with scheduling retrieval scope safe --- .../Document/ByKeyDocumentController.cs | 41 ++++++++++------- .../Document/DocumentControllerBase.cs | 11 +++++ .../Factories/DocumentPresentationFactory.cs | 2 - .../DependencyInjection/UmbracoBuilder.cs | 2 + .../ContentScheduleQueryResult.cs | 14 ++++++ .../Services/ContentPublishingService.cs | 3 -- .../ContentQueryOperationStatus.cs | 8 ++++ .../Services/Querying/ContentQueryService.cs | 45 +++++++++++++++++++ 8 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 src/Umbraco.Core/Models/ContentQuery/ContentScheduleQueryResult.cs create mode 100644 src/Umbraco.Core/Services/OperationStatus/ContentQueryOperationStatus.cs create mode 100644 src/Umbraco.Core/Services/Querying/ContentQueryService.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs index 32858213cbe3..a8f92bbcc767 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs @@ -4,13 +4,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; -using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Querying; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -20,9 +19,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document; public class ByKeyDocumentController : DocumentControllerBase { private readonly IAuthorizationService _authorizationService; - private readonly IContentEditingService _contentEditingService; private readonly IDocumentPresentationFactory _documentPresentationFactory; - private readonly IContentService _contentService; + private readonly IContentQueryService _contentQueryService; [Obsolete("Scheduled for removal in v17")] public ByKeyDocumentController( @@ -31,22 +29,32 @@ public ByKeyDocumentController( IDocumentPresentationFactory documentPresentationFactory) { _authorizationService = authorizationService; - _contentEditingService = contentEditingService; _documentPresentationFactory = documentPresentationFactory; - _contentService = StaticServiceProvider.Instance.GetRequiredService(); + _contentQueryService = StaticServiceProvider.Instance.GetRequiredService(); } - [ActivatorUtilitiesConstructor] + // needed for greedy selection until other constructor remains in v17 + [Obsolete("Scheduled for removal in v17")] public ByKeyDocumentController( IAuthorizationService authorizationService, IContentEditingService contentEditingService, IDocumentPresentationFactory documentPresentationFactory, - IContentService contentService) + IContentQueryService contentQueryService) { _authorizationService = authorizationService; - _contentEditingService = contentEditingService; _documentPresentationFactory = documentPresentationFactory; - _contentService = contentService; + _contentQueryService = contentQueryService; + } + + [ActivatorUtilitiesConstructor] + public ByKeyDocumentController( + IAuthorizationService authorizationService, + IDocumentPresentationFactory documentPresentationFactory, + IContentQueryService contentQueryService) + { + _authorizationService = authorizationService; + _documentPresentationFactory = documentPresentationFactory; + _contentQueryService = contentQueryService; } [HttpGet("{id:guid}")] @@ -65,15 +73,16 @@ public async Task ByKey(CancellationToken cancellationToken, Guid return Forbidden(); } - IContent? content = await _contentEditingService.GetAsync(id); - if (content == null) + var contentWithScheduleAttempt = await _contentQueryService.GetWithSchedulesAsync(id); + + if (contentWithScheduleAttempt.Success == false) { - return DocumentNotFound(); + return ContentQueryOperationStatusResult(contentWithScheduleAttempt.Status); } - ContentScheduleCollection schedule = _contentService.GetContentScheduleByContentId(content.Id); - - DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content, schedule); + DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync( + contentWithScheduleAttempt.Result!.Content, + contentWithScheduleAttempt.Result.Schedules); return Ok(model); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs index 1159521f9fa3..8789eab68797 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs @@ -170,4 +170,15 @@ protected IActionResult PublicAccessOperationStatusResult(PublicAccessOperationS .WithTitle("Unknown content operation status.") .Build()), }); + + protected IActionResult ContentQueryOperationStatusResult(ContentQueryOperationStatus status) + => OperationStatusResult(status, problemDetailsBuilder => status switch + { + ContentQueryOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder + .WithTitle("The document could not be found") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder + .WithTitle("Unknown content query status.") + .Build()), + }); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index 4d46a549ea29..f4d7d3f3d70e 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -216,8 +216,6 @@ public Attempt Create }); } - // NOTE: Keep the default implementation on the interface in line with this one until - // the default implementation can be removed public Attempt, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels(PublishDocumentRequestModel requestModel) { var model = new List(); diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 5e06ea504208..030207690e5e 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -40,6 +40,7 @@ using Umbraco.Cms.Core.Services.FileSystem; using Umbraco.Cms.Core.Services.ImportExport; using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Core.Services.Querying; using Umbraco.Cms.Core.Services.Querying.RecycleBin; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; @@ -416,6 +417,7 @@ private void AddCoreServices() // Add Query services Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); // Authorizers Services.AddSingleton(); diff --git a/src/Umbraco.Core/Models/ContentQuery/ContentScheduleQueryResult.cs b/src/Umbraco.Core/Models/ContentQuery/ContentScheduleQueryResult.cs new file mode 100644 index 000000000000..04fcb154f3ff --- /dev/null +++ b/src/Umbraco.Core/Models/ContentQuery/ContentScheduleQueryResult.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Core.Models.ContentQuery; + +public class ContentScheduleQueryResult +{ + public ContentScheduleQueryResult(IContent content, ContentScheduleCollection schedules) + { + Content = content; + Schedules = schedules; + } + + public IContent Content { get; init; } + + public ContentScheduleCollection Schedules { get; init; } +} diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index f59b0f2482cc..218a7ec2dc05 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -33,9 +33,6 @@ public ContentPublishingService( _languageService = languageService; } - // NOTE: Keep the default implementation on the interface in line with this one until - // the default implementation can be removed - /// public async Task> PublishAsync( Guid key, diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentQueryOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentQueryOperationStatus.cs new file mode 100644 index 000000000000..6dcf7433e6d8 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/ContentQueryOperationStatus.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum ContentQueryOperationStatus +{ + Success, + ContentNotFound, + Unknown, +} diff --git a/src/Umbraco.Core/Services/Querying/ContentQueryService.cs b/src/Umbraco.Core/Services/Querying/ContentQueryService.cs new file mode 100644 index 000000000000..241236f5509d --- /dev/null +++ b/src/Umbraco.Core/Services/Querying/ContentQueryService.cs @@ -0,0 +1,45 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentQuery; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.Querying; + +public interface IContentQueryService +{ + Task> GetWithSchedulesAsync(Guid id); +} + +public class ContentQueryService : IContentQueryService +{ + private readonly IContentService _contentService; + private readonly ICoreScopeProvider _coreScopeProvider; + + public ContentQueryService( + IContentService contentService, + ICoreScopeProvider coreScopeProvider) + { + _contentService = contentService; + _coreScopeProvider = coreScopeProvider; + } + + public async Task> GetWithSchedulesAsync(Guid id) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + + IContent? content = await Task.FromResult(_contentService.GetById(id)); + + if (content == null) + { + return Attempt.Fail(ContentQueryOperationStatus + .ContentNotFound); + } + + ContentScheduleCollection schedules = _contentService.GetContentScheduleByContentId(id); + + return Attempt + .Succeed( + ContentQueryOperationStatus.Success, + new ContentScheduleQueryResult(content, schedules)); + } +}