From e425f0ba4160575961092baa1b00fb8a3c1b1e27 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Wed, 8 Jan 2025 12:39:34 +0100 Subject: [PATCH] Improve document schedule (#17535) * Expose schedule date for on document get endpoint * typo fix * stupid stuff * Enable content scheduling features in the publishing service * Replace obsoleted non async calls * Add content scheduling test * Publush and schedule combination test * More invariantCulture notation allignment and more tests * Link up api with updated document scheduling * More invariant culture notation allignment * Fix breaking change * Return expected status codes. * Fix constructor * Forward Default implementation to actual core implementation Co-authored-by: Bjarke Berg * Forward default implementation to core implementation Co-authored-by: Bjarke Berg * Make content with scheduling retrieval scope safe --------- Co-authored-by: Bjarke Berg --- .../Document/ByKeyDocumentController.cs | 45 +- .../Document/DocumentControllerBase.cs | 11 + .../Document/PublishDocumentController.cs | 4 +- .../Factories/DocumentPresentationFactory.cs | 73 +- .../Factories/IDocumentPresentationFactory.cs | 61 +- .../Mapping/Document/DocumentMapDefinition.cs | 23 + .../Document/DocumentVariantResponseModel.cs | 4 + .../Document/PublishDocumentRequestModel.cs | 1 - src/Umbraco.Core/Constants-System.cs | 2 + .../DependencyInjection/UmbracoBuilder.cs | 2 + .../Extensions/ContentExtensions.cs | 6 +- .../CultureAndScheduleModel.cs | 2 - .../ContentPublishing/CultureScheduleModel.cs | 21 + .../ContentScheduleQueryResult.cs | 14 + .../Models/ContentScheduleCollection.cs | 34 +- .../Services/ContentPublishingService.cs | 74 +- src/Umbraco.Core/Services/ContentService.cs | 59 +- .../Services/IContentPublishingService.cs | 17 + src/Umbraco.Core/Services/IContentService.cs | 6 + .../ContentQueryOperationStatus.cs | 8 + .../Services/Querying/ContentQueryService.cs | 45 + .../Implement/DocumentRepository.cs | 2 +- .../Services/ContentEditingServiceTests.cs | 6 +- .../Services/ContentPublishingServiceTests.cs | 1558 +++++++++++++++++ .../Services/ContentServiceTests.cs | 2 +- .../Models/ContentScheduleTests.cs | 5 +- 26 files changed, 2045 insertions(+), 40 deletions(-) create mode 100644 src/Umbraco.Core/Models/ContentPublishing/CultureScheduleModel.cs 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 create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs index b805633b66..a8f92bbcc7 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs @@ -2,13 +2,14 @@ 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.Models; +using Umbraco.Cms.Core.DependencyInjection; 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; @@ -18,17 +19,42 @@ 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 IContentQueryService _contentQueryService; + [Obsolete("Scheduled for removal in v17")] public ByKeyDocumentController( IAuthorizationService authorizationService, IContentEditingService contentEditingService, IDocumentPresentationFactory documentPresentationFactory) { _authorizationService = authorizationService; - _contentEditingService = contentEditingService; _documentPresentationFactory = documentPresentationFactory; + _contentQueryService = StaticServiceProvider.Instance.GetRequiredService(); + } + + // needed for greedy selection until other constructor remains in v17 + [Obsolete("Scheduled for removal in v17")] + public ByKeyDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IDocumentPresentationFactory documentPresentationFactory, + IContentQueryService contentQueryService) + { + _authorizationService = authorizationService; + _documentPresentationFactory = documentPresentationFactory; + _contentQueryService = contentQueryService; + } + + [ActivatorUtilitiesConstructor] + public ByKeyDocumentController( + IAuthorizationService authorizationService, + IDocumentPresentationFactory documentPresentationFactory, + IContentQueryService contentQueryService) + { + _authorizationService = authorizationService; + _documentPresentationFactory = documentPresentationFactory; + _contentQueryService = contentQueryService; } [HttpGet("{id:guid}")] @@ -47,13 +73,16 @@ public class ByKeyDocumentController : DocumentControllerBase 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); } - DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content); + 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 1159521f9f..8789eab687 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 @@ public abstract class DocumentControllerBase : ContentControllerBase .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/Controllers/Document/PublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs index 3c39270a3e..fe29aa77cd 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 class PublishDocumentController : DocumentControllerBase { 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 class PublishDocumentController : DocumentControllerBase 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 463dbbd5ab..f4d7d3f3d7 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -40,6 +40,7 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory _idKeyMap = idKeyMap; } + [Obsolete("Schedule for removal in v17")] public async Task CreateResponseModelAsync(IContent content) { DocumentResponseModel responseModel = _umbracoMapper.Map(content)!; @@ -74,6 +75,24 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory 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) { Attempt parentKeyAttempt = _idKeyMap.GetKeyForId(entity.ParentId, UmbracoObjectTypes.Document); @@ -135,6 +154,7 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory 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(); @@ -143,7 +163,7 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory { 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; } @@ -159,7 +179,7 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory } contentScheduleCollection.Add(new ContentSchedule( - cultureAndScheduleRequestModel.Culture ?? "*", + cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture, cultureAndScheduleRequestModel.Schedule.PublishTime.Value.UtcDateTime, ContentScheduleAction.Release)); } @@ -184,7 +204,7 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory } contentScheduleCollection.Add(new ContentSchedule( - cultureAndScheduleRequestModel.Culture ?? "*", + cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture, cultureAndScheduleRequestModel.Schedule.UnpublishTime.Value.UtcDateTime, ContentScheduleAction.Expire)); } @@ -195,4 +215,51 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory CulturesToPublishImmediately = culturesToPublishImmediately, }); } + + 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 40a537e01a..ae725e97a9 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; @@ -12,10 +14,17 @@ namespace Umbraco.Cms.Api.Management.Factories; public interface IDocumentPresentationFactory { + [Obsolete("Schedule for removal in v17")] 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 + => CreateResponseModelAsync(content); +#pragma warning restore CS0618 // Type or member is obsolete + DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity); DocumentBlueprintItemResponseModel CreateBlueprintItemResponseModel(IDocumentEntitySlim entity); @@ -24,5 +33,55 @@ public interface IDocumentPresentationFactory 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); + } } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs index 228cffbce3..ec40a8de27 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs @@ -25,6 +25,7 @@ public class DocumentMapDefinition : ContentMapDefinition((_, _) => new PublishedDocumentResponseModel(), Map); mapper.Define((_, _) => new DocumentCollectionResponseModel(), Map); mapper.Define((_, _) => new DocumentBlueprintResponseModel(), Map); + mapper.Define(Map); } // Umbraco.Code.MapAll -Urls -Template @@ -113,4 +114,26 @@ public class DocumentMapDefinition : ContentMapDefinition 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 db1f2e67b6..58b4768cde 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; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs index 0bf1ba98be..64cac6202e 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 a94a1cd583..cf7c1de325 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -89,5 +89,7 @@ public static partial class Constants /// The DataDirectory placeholder. /// public const string DataDirectoryPlaceholder = "|DataDirectory|"; + + public const string InvariantCulture = "*"; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index f5157271c5..78692dff98 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -39,6 +39,7 @@ using Umbraco.Cms.Core.Serialization; 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; @@ -418,6 +419,7 @@ namespace Umbraco.Cms.Core.DependencyInjection // Add Query services Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); // Authorizers Services.AddSingleton(); diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index 7d0bba26f8..dde4ffb397 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -237,20 +237,20 @@ public static class ContentExtensions 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/ContentPublishing/CultureAndScheduleModel.cs b/src/Umbraco.Core/Models/ContentPublishing/CultureAndScheduleModel.cs index 0d4882ea85..42a586508b 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 0000000000..5b00bfc798 --- /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 ContentScheduleModel? Schedule { get; set; } +} + +public class ContentScheduleModel +{ + public DateTimeOffset? PublishDate { get; set; } + + public DateTimeOffset? UnpublishDate { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentQuery/ContentScheduleQueryResult.cs b/src/Umbraco.Core/Models/ContentQuery/ContentScheduleQueryResult.cs new file mode 100644 index 0000000000..04fcb154f3 --- /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/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index 4fb90779de..8dbdd76e79 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -64,7 +64,7 @@ public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneabl 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 class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneabl /// /// /// - 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 @@ -170,13 +170,39 @@ public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneabl } } + 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 /// /// /// 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 @@ -226,7 +252,7 @@ public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneabl /// /// 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/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 24c73271ee..218a7ec2dc 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -34,6 +34,50 @@ internal sealed class ContentPublishingService : IContentPublishingService } /// + 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 @@ internal sealed class ContentPublishingService : IContentPublishingService return Attempt.FailWithStatus(ContentPublishingOperationStatus.ContentNotFound, new ContentPublishingResult()); } + // clear all schedules and publish nothing + if (cultureAndSchedule.CulturesToPublishImmediately.Count == 0 && + cultureAndSchedule.Schedules.FullSchedule.Count == 0) + { + _contentService.PersistContentSchedule(content, cultureAndSchedule.Schedules); + scope.Complete(); + return Attempt.SucceedWithStatus( + ContentPublishingOperationStatus.Success, + new ContentPublishingResult { Content = content }); + } var cultures = cultureAndSchedule.CulturesToPublishImmediately.Union( @@ -60,8 +114,14 @@ internal sealed class ContentPublishingService : IContentPublishingService return Attempt.FailWithStatus(ContentPublishingOperationStatus.CultureMissing, new ContentPublishingResult()); } + if (cultures.Any(x => 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 == "*") || cultures.All(x=> validCultures.Contains(x) is false)) + if (validCultures.ContainsAll(cultures) is false) { scope.Complete(); return Attempt.FailWithStatus(ContentPublishingOperationStatus.InvalidCulture, new ContentPublishingResult()); @@ -69,7 +129,7 @@ internal sealed class ContentPublishingService : IContentPublishingService } else { - if (cultures.Length != 1 || cultures.Any(x => x != "*")) + if (cultures.Length != 1 || cultures.Any(x => x != Constants.System.InvariantCulture)) { scope.Complete(); return Attempt.FailWithStatus(ContentPublishingOperationStatus.InvalidCulture, new ContentPublishingResult()); @@ -95,10 +155,14 @@ internal sealed class ContentPublishingService : IContentPublishingService { result = _contentService.Publish(content, cultureAndSchedule.CulturesToPublishImmediately.ToArray(), userId); } - else if(cultureAndSchedule.Schedules.FullSchedule.Any()) + + if (result?.Success != false && cultureAndSchedule.Schedules.FullSchedule.Any()) { - _contentService.PersistContentSchedule(content, cultureAndSchedule.Schedules); - result = new PublishResult(PublishResultType.SuccessPublish, new EventMessages(), content); + _contentService.PersistContentSchedule(result?.Content ?? content, cultureAndSchedule.Schedules); + result = new PublishResult( + PublishResultType.SuccessPublish, + result?.EventMessages ?? new EventMessages(), + result?.Content ?? content); } scope.Complete(); diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 473de72a76..3cf1d86ce9 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -13,7 +13,6 @@ using Umbraco.Cms.Core.Persistence.Repositories; 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; @@ -36,6 +35,7 @@ public class ContentService : RepositoryService, IContentService private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly PropertyEditorCollection _propertyEditorCollection; + private readonly IIdKeyMap _idKeyMap; private IQuery? _queryNotTrashed; #region Constructors @@ -54,7 +54,8 @@ public class ContentService : RepositoryService, IContentService IShortStringHelper shortStringHelper, ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, - PropertyEditorCollection propertyEditorCollection) + PropertyEditorCollection propertyEditorCollection, + IIdKeyMap idKeyMap) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -68,9 +69,45 @@ public class ContentService : RepositoryService, IContentService _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, + IEventMessagesFactory eventMessagesFactory, + IDocumentRepository documentRepository, + IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, + Lazy propertyValidationService, + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver, + PropertyEditorCollection propertyEditorCollection) + : this( + provider, + loggerFactory, + eventMessagesFactory, + documentRepository, + entityRepository, + auditRepository, + contentTypeRepository, + documentBlueprintRepository, + languageRepository, + propertyValidationService, + shortStringHelper, + cultureImpactFactory, + userIdKeyResolver, + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] public ContentService( ICoreScopeProvider provider, @@ -100,7 +137,8 @@ public class ContentService : RepositoryService, IContentService shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService() + ) { } @@ -550,6 +588,17 @@ public class ContentService : RepositoryService, IContentService } } + 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) { @@ -3176,8 +3225,8 @@ public class ContentService : RepositoryService, IContentService 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/src/Umbraco.Core/Services/IContentPublishingService.cs b/src/Umbraco.Core/Services/IContentPublishingService.cs index 701aba3166..73fc668543 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,17 @@ 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. + /// + Task> PublishAsync( + Guid key, + ICollection culturesToPublishOrSchedule, + Guid userKey) => StaticServiceProvider.Instance.GetRequiredService() + .PublishAsync(key, culturesToPublishOrSchedule, userKey); } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 344a5c8551..dbc9868003 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; + using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; @@ -539,4 +542,7 @@ public interface IContentService : IContentServiceBase #endregion Task EmptyRecycleBinAsync(Guid userId); + +ContentScheduleCollection GetContentScheduleByContentId(Guid contentId) => StaticServiceProvider.Instance + .GetRequiredService().GetContentScheduleByContentId(contentId); } diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentQueryOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentQueryOperationStatus.cs new file mode 100644 index 0000000000..6dcf7433e6 --- /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 0000000000..241236f550 --- /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)); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 2a80dde09a..72459bd755 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -117,7 +117,7 @@ public class DocumentRepository : ContentRepositoryBase GetRequiredService(); + private ITemplateService TemplateService => GetRequiredService(); + [Test] public async Task Only_Supplied_Cultures_Are_Updated() { @@ -107,7 +109,7 @@ public class ContentEditingServiceTests : UmbracoIntegrationTestWithContent 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 class ContentEditingServiceTests : UmbracoIntegrationTestWithContent .Build(); contentType.AllowedAsRoot = true; - ContentTypeService.Save(contentType); + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); return (langEn, langDa, contentType); } 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 0000000000..0853bca8c5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.cs @@ -0,0 +1,1558 @@ +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); + Assert.AreEqual(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant, publishAttempt.Status); + + 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); + } + //todo more tests for invariant + //todo update schedule date + + [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); + Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, publishAttempt.Status); + + 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 ContentScheduleModel { 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); + Assert.AreEqual(ContentPublishingOperationStatus.CultureAwaitingRelease, publishAttempt.Status); + + var content = ContentService.GetById(setupData.Key); + Assert.AreEqual(0, content!.PublishedCultures.Count()); + } + + #endregion + + #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() + { + 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 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.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 ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + 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.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 ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + 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.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 ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate } + }, + }, + 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); + + 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_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() + { + 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 ContentScheduleModel { 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 ContentScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { 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 ContentScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ContentScheduleModel { 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 ContentScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ContentScheduleModel { UnpublishDate = _schedulePublishDate } + }, + }, + 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); + + 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_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() + { + 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 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.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 ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate } + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + 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.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 ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + 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.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 ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ContentScheduleModel { UnpublishDate = _scheduleUnPublishDate }, + }, + }, + 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); + + 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_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() + { + 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 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.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 ContentScheduleModel { PublishDate = _schedulePublishDate } + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + 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.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 ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + 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.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 ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + new() + { + Culture = UnknownCulture, + Schedule = new ContentScheduleModel { PublishDate = _schedulePublishDate }, + }, + }, + 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); + + 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_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() + { + 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 ContentScheduleModel(), + }, + }, + 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 ContentScheduleModel(), + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel(), + }, + }, + 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 ContentScheduleModel(), + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = new ContentScheduleModel(), + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ContentScheduleModel(), + }, + }, + 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 + + [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 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(1, content!.PublishedCultures.Count()); + Assert.AreEqual(1, schedules.FullSchedule.Count); + }); + } + #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 ContentScheduleModel + { + PublishDate = _schedulePublishDate, UnpublishDate = _scheduleUnPublishDate, + }, + }, + new() + { + Culture = setupInfo.LangDa.IsoCode, + Schedule = + new ContentScheduleModel + { + PublishDate = _schedulePublishDate, UnpublishDate = _scheduleUnPublishDate, + }, + }, + new() + { + Culture = setupInfo.LangBe.IsoCode, + Schedule = new ContentScheduleModel + { + PublishDate = _schedulePublishDate, UnpublishDate = _scheduleUnPublishDate, + }, + }, + }, + Constants.Security.SuperUserKey); + + 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 +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 352400c4dd..625068adae 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 class ContentServiceTests : UmbracoIntegrationTestWithContent 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 78bfa0a3f8..a6958ae930 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 class ContentScheduleTests 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 class ContentScheduleTests 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());