diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs index a38ade6060..e41f16cee6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs @@ -121,15 +121,4 @@ public abstract class ContentCollectionControllerBase - /// Populates the signs for the collection response models. - /// - protected async Task PopulateSigns(IEnumerable itemViewModels) - { - foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) - { - await signProvider.PopulateSignsAsync(itemViewModels); - } - } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs index 5ab89ba274..83ef32972c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs @@ -85,7 +85,6 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas } List collectionResponseModels = await _documentCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!); - await PopulateSigns(collectionResponseModels); return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs index f67f880e57..d65183c824 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs @@ -17,25 +17,14 @@ public class ItemDocumentItemController : DocumentItemControllerBase { private readonly IEntityService _entityService; private readonly IDocumentPresentationFactory _documentPresentationFactory; - private readonly SignProviderCollection _signProviders; [ActivatorUtilitiesConstructor] public ItemDocumentItemController( IEntityService entityService, - IDocumentPresentationFactory documentPresentationFactory, - SignProviderCollection signProvider) + IDocumentPresentationFactory documentPresentationFactory) { _entityService = entityService; _documentPresentationFactory = documentPresentationFactory; - _signProviders = signProvider; - } - - [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18")] - public ItemDocumentItemController( - IEntityService entityService, - IDocumentPresentationFactory documentPresentationFactory) - : this(entityService, documentPresentationFactory, StaticServiceProvider.Instance.GetRequiredService()) - { } [HttpGet] @@ -55,15 +44,6 @@ public class ItemDocumentItemController : DocumentItemControllerBase .OfType(); IEnumerable responseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel); - await PopulateSigns(responseModels); return Ok(responseModels); } - - private async Task PopulateSigns(IEnumerable itemViewModels) - { - foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) - { - await signProvider.PopulateSignsAsync(itemViewModels); - } - } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs index 184cfe4de0..7029e9311d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs @@ -84,7 +84,6 @@ public class ByKeyMediaCollectionController : MediaCollectionControllerBase } List collectionResponseModels = await _mediaCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!); - await PopulateSigns(collectionResponseModels); return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total); } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs index 0607df5108..f151d6337b 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -13,9 +16,24 @@ public abstract class ContentCollectionPresentationFactory _mapper = mapper; + [Obsolete("Please use the controller with all parameters, will be removed in Umbraco 18")] + protected ContentCollectionPresentationFactory(IUmbracoMapper mapper) + : this( + mapper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + protected ContentCollectionPresentationFactory( + IUmbracoMapper mapper, + SignProviderCollection signProviderCollection) + { + _mapper = mapper; + _signProviderCollection = signProviderCollection; + } public async Task> CreateCollectionModelAsync(ListViewPagedModel contentCollection) { @@ -36,8 +54,19 @@ public abstract class ContentCollectionPresentationFactory contentCollection, List collectionResponseModels) => Task.CompletedTask; + + private async Task PopulateSigns(IEnumerable models) + { + foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns())) + { + await signProvider.PopulateSignsAsync(models); + } + } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index f0e3c9901e..37c127a240 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -1,10 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels; 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.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; @@ -23,7 +26,9 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory private readonly IPublicAccessService _publicAccessService; private readonly TimeProvider _timeProvider; private readonly IIdKeyMap _idKeyMap; + private readonly SignProviderCollection _signProviderCollection; + [Obsolete("Please use the controller with all parameters. Scheduled for removal in Umbraco 18")] public DocumentPresentationFactory( IUmbracoMapper umbracoMapper, IDocumentUrlFactory documentUrlFactory, @@ -31,6 +36,25 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory IPublicAccessService publicAccessService, TimeProvider timeProvider, IIdKeyMap idKeyMap) + : this( + umbracoMapper, + documentUrlFactory, + templateService, + publicAccessService, + timeProvider, + idKeyMap, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public DocumentPresentationFactory( + IUmbracoMapper umbracoMapper, + IDocumentUrlFactory documentUrlFactory, + ITemplateService templateService, + IPublicAccessService publicAccessService, + TimeProvider timeProvider, + IIdKeyMap idKeyMap, + SignProviderCollection signProviderCollection) { _umbracoMapper = umbracoMapper; _documentUrlFactory = documentUrlFactory; @@ -38,6 +62,7 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory _publicAccessService = publicAccessService; _timeProvider = timeProvider; _idKeyMap = idKeyMap; + _signProviderCollection = signProviderCollection; } [Obsolete("Schedule for removal in v17")] @@ -105,6 +130,8 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory responseModel.Variants = CreateVariantsItemResponseModels(entity); + PopulateSignsOnDocuments(responseModel); + return responseModel; } @@ -125,23 +152,29 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory { if (entity.Variations.VariesByCulture() is false) { - yield return new() + var model = new DocumentVariantItemResponseModel() { Name = entity.Name ?? string.Empty, State = DocumentVariantStateHelper.GetState(entity, null), Culture = null, }; + + PopulateSignsOnVariants(model); + yield return model; yield break; } foreach (KeyValuePair cultureNamePair in entity.CultureNames) { - yield return new() + var model = new DocumentVariantItemResponseModel() { Name = cultureNamePair.Value, Culture = cultureNamePair.Key, State = DocumentVariantStateHelper.GetState(entity, cultureNamePair.Key) }; + + PopulateSignsOnVariants(model); + yield return model; } } @@ -256,4 +289,20 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model); } + + private void PopulateSignsOnDocuments(DocumentItemResponseModel model) + { + foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns())) + { + signProvider.PopulateSignsAsync([model]).GetAwaiter().GetResult(); + } + } + + private void PopulateSignsOnVariants(DocumentVariantItemResponseModel model) + { + foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns())) + { + signProvider.PopulateSignsAsync([model]).GetAwaiter().GetResult(); + } + } } diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs index 50c421a792..e7563aa310 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs @@ -1,8 +1,5 @@ using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; -using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; -using Umbraco.Cms.Api.Management.ViewModels.Document.Item; -using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; namespace Umbraco.Cms.Api.Management.Services.Signs; @@ -17,15 +14,15 @@ public class HasPendingChangesSignProvider : ISignProvider /// public bool CanProvideSigns() where TItem : IHasSigns => - typeof(TItem) == typeof(DocumentTreeItemResponseModel) || - typeof(TItem) == typeof(DocumentCollectionResponseModel) || - typeof(TItem) == typeof(DocumentItemResponseModel); + typeof(TItem) == typeof(DocumentVariantItemResponseModel) || + typeof(TItem) == typeof(DocumentVariantResponseModel); + /// - public Task PopulateSignsAsync(IEnumerable itemViewModels) + public Task PopulateSignsAsync(IEnumerable items) where TItem : IHasSigns { - foreach (TItem item in itemViewModels) + foreach (TItem item in items) { if (HasPendingChanges(item)) { @@ -39,11 +36,10 @@ public class HasPendingChangesSignProvider : ISignProvider /// /// Determines if the given item has any variant that has pending changes. /// - private bool HasPendingChanges(object item) => item switch + private static bool HasPendingChanges(object item) => item switch { - DocumentTreeItemResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, - DocumentCollectionResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, - DocumentItemResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, + DocumentVariantItemResponseModel variant => variant.State == DocumentVariantState.PublishedPendingChanges, + DocumentVariantResponseModel variant => variant.State == DocumentVariantState.PublishedPendingChanges, _ => false, }; } diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs index 599d10ae67..e9f949a57a 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs @@ -1,7 +1,10 @@ using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Api.Management.ViewModels.Document.Item; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Constants = Umbraco.Cms.Core.Constants; @@ -15,11 +18,16 @@ internal class HasScheduleSignProvider : ISignProvider private const string Alias = Constants.Conventions.Signs.Prefix + "ScheduledForPublish"; private readonly IContentService _contentService; + private readonly IIdKeyMap _keyMap; /// /// Initializes a new instance of the class. /// - public HasScheduleSignProvider(IContentService contentService) => _contentService = contentService; + public HasScheduleSignProvider(IContentService contentService, IIdKeyMap keyMap) + { + _contentService = contentService; + _keyMap = keyMap; + } /// public bool CanProvideSigns() @@ -29,15 +37,89 @@ internal class HasScheduleSignProvider : ISignProvider typeof(TItem) == typeof(DocumentItemResponseModel); /// - public Task PopulateSignsAsync(IEnumerable itemViewModels) + public Task PopulateSignsAsync(IEnumerable items) where TItem : IHasSigns { - IEnumerable contentKeysScheduledForPublishing = _contentService.GetScheduledContentKeys(itemViewModels.Select(x => x.Id)); - foreach (Guid key in contentKeysScheduledForPublishing) + IDictionary> schedules = _contentService.GetContentSchedulesByIds(items.Select(x => x.Id).ToArray()); + foreach (TItem item in items) { - itemViewModels.First(x => x.Id == key).AddSign(Alias); + Attempt itemId = _keyMap.GetIdForKey(item.Id, UmbracoObjectTypes.Document); + if (itemId.Success is false) + { + continue; + } + + if (!schedules.TryGetValue(itemId.Result, out IEnumerable? contentSchedules)) + { + continue; + } + + switch (item) + { + case DocumentTreeItemResponseModel documentTreeItemResponseModel: + documentTreeItemResponseModel.Variants = PopulateVariants(documentTreeItemResponseModel.Variants, contentSchedules); + break; + + case DocumentCollectionResponseModel documentCollectionResponseModel: + documentCollectionResponseModel.Variants = PopulateVariants(documentCollectionResponseModel.Variants, contentSchedules); + break; + + case DocumentItemResponseModel documentItemResponseModel: + documentItemResponseModel.Variants = PopulateVariants(documentItemResponseModel.Variants, contentSchedules); + break; + } } return Task.CompletedTask; } + + private IEnumerable PopulateVariants( + IEnumerable variants, IEnumerable schedules) + { + DocumentVariantItemResponseModel[] variantsArray = variants.ToArray(); + if (variantsArray.Length == 1) + { + DocumentVariantItemResponseModel variant = variantsArray[0]; + variant.AddSign(Alias); + return variantsArray; + } + + foreach (DocumentVariantItemResponseModel variant in variantsArray) + { + ContentSchedule? schedule = schedules.FirstOrDefault(x => x.Culture == variant.Culture); + bool isScheduled = schedule != null && schedule.Date > DateTime.Now && string.Equals(schedule.Culture, variant.Culture); + + if (isScheduled) + { + variant.AddSign(Alias); + } + } + + return variantsArray; + } + + private IEnumerable PopulateVariants( + IEnumerable variants, IEnumerable schedules) + { + DocumentVariantResponseModel[] variantsArray = variants.ToArray(); + if (variantsArray.Length == 1) + { + DocumentVariantResponseModel variant = variantsArray[0]; + variant.AddSign(Alias); + return variantsArray; + } + + foreach (DocumentVariantResponseModel variant in variantsArray) + { + ContentSchedule? schedule = schedules.FirstOrDefault(x => x.Culture == variant.Culture); + bool isScheduled = schedule != null && schedule.Date > DateTime.Now && string.Equals(schedule.Culture, variant.Culture); + + if (isScheduled) + { + variant.AddSign(Alias); + } + } + + return variantsArray; + } } diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs index e0324f05c8..e47a128e74 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs @@ -18,7 +18,6 @@ public interface ISignProvider /// Populates the provided item view models with signs. /// /// Type of item view model supporting signs. - /// The collection of item view models to be populated with signs. - Task PopulateSignsAsync(IEnumerable itemViewModels) + Task PopulateSignsAsync(IEnumerable items) where TItem : IHasSigns; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs index ccdc16ec0d..3e4d8913f8 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs @@ -2,7 +2,25 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Document; -public class DocumentVariantItemResponseModel : VariantItemResponseModelBase +public class DocumentVariantItemResponseModel : VariantItemResponseModelBase, IHasSigns { + private readonly List _signs = []; + + public Guid Id { get; } + + public IEnumerable Signs + { + get => _signs.AsEnumerable(); + set + { + _signs.Clear(); + _signs.AddRange(value); + } + } + + public void AddSign(string alias) => _signs.Add(new SignModel { Alias = alias }); + + public void RemoveSign(string alias) => _signs.RemoveAll(x => x.Alias == alias); + public required DocumentVariantState State { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs index b6990c1b3c..cfa3ef5809 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs @@ -2,7 +2,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Content; namespace Umbraco.Cms.Api.Management.ViewModels.Document; -public class DocumentVariantResponseModel : VariantResponseModelBase +public class DocumentVariantResponseModel : VariantResponseModelBase, IHasSigns { public DocumentVariantState State { get; set; } @@ -11,4 +11,22 @@ public class DocumentVariantResponseModel : VariantResponseModelBase public DateTimeOffset? ScheduledPublishDate { get; set; } public DateTimeOffset? ScheduledUnpublishDate { get; set; } + + private readonly List _signs = []; + + public Guid Id { get; } + + public IEnumerable Signs + { + get => _signs.AsEnumerable(); + set + { + _signs.Clear(); + _signs.AddRange(value); + } + } + + public void AddSign(string alias) => _signs.Add(new SignModel { Alias = alias }); + + public void RemoveSign(string alias) => _signs.RemoveAll(x => x.Alias == alias); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index 9ff75c2b3d..6ac6470a85 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -53,11 +54,11 @@ public interface IDocumentRepository : IContentRepository, IReadR /// /// Gets the content keys from the provided collection of keys that are scheduled for publishing. /// - /// The content keys. + /// The IDs of the documents. /// /// The provided collection of content keys filtered for those that are scheduled for publishing. /// - IEnumerable GetScheduledContentKeys(Guid[] keys) => []; + IDictionary> GetContentSchedulesByIds(int[] documentIds) => ImmutableDictionary>.Empty; /// /// Get the count of published items diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 016d4a83ca..5d4ed2f98c 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Microsoft.Extensions.DependencyInjection; @@ -1009,18 +1010,29 @@ public class ContentService : RepositoryService, IContentService /// - public IEnumerable GetScheduledContentKeys(IEnumerable keys) + public IDictionary> GetContentSchedulesByIds(Guid[] keys) { - Guid[] idsA = keys.ToArray(); - if (idsA.Length == 0) + if (keys.Length == 0) { - return Enumerable.Empty(); + return ImmutableDictionary>.Empty; + } + + List contentIds = []; + foreach (var key in keys) + { + Attempt contentId = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document); + if (contentId.Success is false) + { + continue; + } + + contentIds.Add(contentId.Result); } using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.GetScheduledContentKeys(idsA); + return _documentRepository.GetContentSchedulesByIds(contentIds.ToArray()); } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 228d7fa6db..280cf1847a 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; @@ -276,13 +277,14 @@ public interface IContentService : IContentServiceBase bool HasChildren(int id); /// - /// Gets the content keys from the provided collection of keys that are scheduled for publishing. + /// Gets a dictionary of content Ids and their matching content schedules. /// /// The content keys. /// - /// The provided collection of content keys filtered for those that are scheduled for publishing. + /// A dictionary with a nodeId and an IEnumerable of matching ContentSchedules. /// - IEnumerable GetScheduledContentKeys(IEnumerable keys) => []; + IDictionary> GetContentSchedulesByIds(Guid[] keys) => ImmutableDictionary>.Empty; + #endregion diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 1279d62186..6d57566321 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1672,24 +1672,30 @@ public class DocumentRepository : ContentRepositoryBase - public IEnumerable GetScheduledContentKeys(Guid[] keys) + public IDictionary> GetContentSchedulesByIds(int[] documentIds) { - var action = ContentScheduleAction.Release.ToString(); - DateTime now = DateTime.UtcNow; + Sql sql = Sql() + .Select() + .From() + .WhereIn(contentScheduleDto => contentScheduleDto.NodeId, documentIds); - Sql sql = SqlContext.Sql(); - sql - .Select(x => x.UniqueId) - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .WhereIn(x => x.UniqueId, keys) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date >= now)); + List? contentScheduleDtos = Database.Fetch(sql); - return Database.Fetch(sql); + IDictionary> dictionary = contentScheduleDtos + .GroupBy(contentSchedule => contentSchedule.NodeId) + .ToDictionary( + group => group.Key, + group => group.Select(scheduleDto => new ContentSchedule( + scheduleDto.Id, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? Constants.System.InvariantCulture, + scheduleDto.Date, + scheduleDto.Action == ContentScheduleAction.Release.ToString() + ? ContentScheduleAction.Release + : ContentScheduleAction.Expire)) + .ToList().AsEnumerable()); // We have to materialize it here, + // to avoid this being used after the scope is disposed. + + return dictionary; } /// diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 0271b77e84..e19221638a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -688,7 +688,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent } [Test] - public void Can_Get_Scheduled_Content_Keys() + public void Can_Get_Content_Schedules_By_Keys() { // Arrange var root = ContentService.GetById(Textpage.Id); @@ -699,11 +699,12 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Publish(content, content.AvailableCultures.ToArray()); // Act - var keys = ContentService.GetScheduledContentKeys([Textpage.Key, Subpage.Key, Subpage2.Key]).ToList(); + var keys = ContentService.GetContentSchedulesByIds([Textpage.Key, Subpage.Key, Subpage2.Key]).ToList(); // Assert Assert.AreEqual(1, keys.Count); - Assert.AreEqual(Subpage.Key, keys.First()); + Assert.AreEqual(keys[0].Key, Subpage.Id); + Assert.AreEqual(keys[0].Value.First().Id, contentSchedule.FullSchedule.First().Id); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs index 7bc7435919..58dcb07c32 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs @@ -11,116 +11,76 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs; internal class HasPendingChangesSignProviderTests { [Test] - public void HasPendingChangesSignProvider_Can_Provide_Document_Tree_Signs() + public void HasPendingChangesSignProvider_Can_Provide_Variant_Item_Signs() { var sut = new HasPendingChangesSignProvider(); - Assert.IsTrue(sut.CanProvideSigns()); + Assert.IsTrue(sut.CanProvideSigns()); } [Test] - public void HasPendingChangesSignProvider_Can_Provide_Document_Collection_Signs() + public void HasPendingChangesSignProvider_Can_Provide_Variant_Signs() { var sut = new HasPendingChangesSignProvider(); - Assert.IsTrue(sut.CanProvideSigns()); + Assert.IsTrue(sut.CanProvideSigns()); } [Test] - public void HasPendingChangesSignProvider_Can_Provide_Document_Item_Signs() - { - var sut = new HasPendingChangesSignProvider(); - Assert.IsTrue(sut.CanProvideSigns()); - } - - [Test] - public async Task HasPendingChangesSignProvider_Should_Populate_Document_Tree_Signs() + public async Task HasPendingChangesSignProvider_Should_Populate_Variant_Item_Signs() { var sut = new HasPendingChangesSignProvider(); - var viewModels = new List + var variants = new List { - new() { Id = Guid.NewGuid() }, new() { - Id = Guid.NewGuid(), Variants = - [ - new() - { - State = DocumentVariantState.PublishedPendingChanges, - Culture = null, - Name = "Test", - }, - ], + State = DocumentVariantState.PublishedPendingChanges, + Culture = null, + Name = "Test", + }, + new() + { + State = DocumentVariantState.Published, + Culture = null, + Name = "Test2", }, }; - await sut.PopulateSignsAsync(viewModels); + await sut.PopulateSignsAsync(variants); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(variants[0].Signs.Count(), 1); + Assert.AreEqual(variants[1].Signs.Count(), 0); - var signModel = viewModels[1].Signs.First(); + var signModel = variants[0].Signs.First(); Assert.AreEqual("Umb.PendingChanges", signModel.Alias); } [Test] - public async Task HasPendingChangesSignProvider_Should_Populate_Document_Collection_Signs() + public async Task HasPendingChangesSignProvider_Should_Populate_Variant_Signs() { var sut = new HasPendingChangesSignProvider(); - var viewModels = new List + var variants = new List { - new() { Id = Guid.NewGuid() }, new() { - Id = Guid.NewGuid(), Variants = - [ - new() - { - State = DocumentVariantState.PublishedPendingChanges, - Culture = null, - Name = "Test", - }, - ], + State = DocumentVariantState.PublishedPendingChanges, + Culture = null, + Name = "Test", + }, + new() + { + State = DocumentVariantState.Published, + Culture = null, + Name = "Test2", }, }; - await sut.PopulateSignsAsync(viewModels); + await sut.PopulateSignsAsync(variants); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(variants[0].Signs.Count(), 1); + Assert.AreEqual(variants[1].Signs.Count(), 0); - var signModel = viewModels[1].Signs.First(); - Assert.AreEqual("Umb.PendingChanges", signModel.Alias); - } - - [Test] - public async Task HasPendingChangesSignProvider_Should_Populate_Document_Item_Signs() - { - var sut = new HasPendingChangesSignProvider(); - - var viewModels = new List - { - new() { Id = Guid.NewGuid() }, - new() - { - Id = Guid.NewGuid(), Variants = - [ - new() - { - State = DocumentVariantState.PublishedPendingChanges, - Culture = null, - Name = "Test", - }, - ], - }, - }; - - await sut.PopulateSignsAsync(viewModels); - - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); - - var signModel = viewModels[1].Signs.First(); + var signModel = variants[0].Signs.First(); Assert.AreEqual("Umb.PendingChanges", signModel.Alias); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs index 48b97ff30b..7292b3a9ae 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs @@ -1,9 +1,13 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Api.Management.ViewModels.Document.Item; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -16,8 +20,9 @@ internal class HasScheduleSignProviderTests public void HasScheduleSignProvider_Can_Provide_Document_Tree_Signs() { var contentServiceMock = new Mock(); + var idKeyMapMock = new Mock(); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); Assert.IsTrue(sut.CanProvideSigns()); } @@ -25,8 +30,9 @@ internal class HasScheduleSignProviderTests public void HasScheduleSignProvider_Can_Provide_Document_Collection_Signs() { var contentServiceMock = new Mock(); + var idKeyMapMock = new Mock(); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); Assert.IsTrue(sut.CanProvideSigns()); } @@ -34,8 +40,9 @@ internal class HasScheduleSignProviderTests public void HasScheduleSignProvider_Can_Provide_Document_Item_Signs() { var contentServiceMock = new Mock(); + var idKeyMapMock = new Mock(); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); Assert.IsTrue(sut.CanProvideSigns()); } @@ -47,23 +54,37 @@ internal class HasScheduleSignProviderTests new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, }; + var idKeyMapMock = new Mock(); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[0].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(1)); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[1].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(2)); + + Guid[] keys = entities.Select(x => x.Key).ToArray(); var contentServiceMock = new Mock(); contentServiceMock - .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) - .Returns([entities[1].Key]); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + .Setup(x => x.GetContentSchedulesByIds(keys)) + .Returns(CreateContentSchedules()); + + + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); + + var variant1 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "en-EN" }; + var variant2 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "da-DA" }; + var variant3 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.PublishedPendingChanges, Name = "Test" }; var viewModels = new List { - new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + new() { Id = entities[0].Key, Variants = [variant1, variant2] }, new() { Id = entities[1].Key, Variants = [variant3] }, }; await sut.PopulateSignsAsync(viewModels); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "da-DA").Signs.Count(), 0); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "en-EN").Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Variants.First().Signs.Count(), 1); - var signModel = viewModels[1].Signs.First(); + var signModel = viewModels[0].Variants.First().Signs.First(); Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); } @@ -75,23 +96,36 @@ internal class HasScheduleSignProviderTests new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, }; + var idKeyMapMock = new Mock(); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[0].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(1)); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[1].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(2)); + + Guid[] keys = entities.Select(x => x.Key).ToArray(); var contentServiceMock = new Mock(); contentServiceMock - .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) - .Returns([entities[1].Key]); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + .Setup(x => x.GetContentSchedulesByIds(keys)) + .Returns(CreateContentSchedules()); + + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); + + var variant1 = new DocumentVariantResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "en-EN" }; + var variant2 = new DocumentVariantResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "da-DA" }; + var variant3 = new DocumentVariantResponseModel() { State = DocumentVariantState.PublishedPendingChanges, Name = "Test" }; var viewModels = new List { - new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + new() { Id = entities[0].Key, Variants = [variant1, variant2] }, new() { Id = entities[1].Key, Variants = [variant3] }, }; await sut.PopulateSignsAsync(viewModels); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "da-DA").Signs.Count(), 0); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "en-EN").Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Variants.First().Signs.Count(), 1); - var signModel = viewModels[1].Signs.First(); + var signModel = viewModels[0].Variants.First().Signs.First(); Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); } @@ -103,23 +137,51 @@ internal class HasScheduleSignProviderTests new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, }; + var idKeyMapMock = new Mock(); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[0].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(1)); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[1].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(2)); + + Guid[] keys = entities.Select(x => x.Key).ToArray(); var contentServiceMock = new Mock(); contentServiceMock - .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) - .Returns([entities[1].Key]); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + .Setup(x => x.GetContentSchedulesByIds(keys)) + .Returns(CreateContentSchedules()); + + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); + + var variant1 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "en-EN" }; + var variant2 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "da-DA" }; + var variant3 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.PublishedPendingChanges, Name = "Test" }; var viewModels = new List { - new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + new() { Id = entities[0].Key, Variants = [variant1, variant2] }, new() { Id = entities[1].Key, Variants = [variant3] }, }; await sut.PopulateSignsAsync(viewModels); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "da-DA").Signs.Count(), 0); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "en-EN").Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Variants.First().Signs.Count(), 1); - var signModel = viewModels[1].Signs.First(); + var signModel = viewModels[0].Variants.First().Signs.First(); Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); } + + private Dictionary> CreateContentSchedules() + { + Dictionary> contentSchedules = new Dictionary>(); + + contentSchedules.Add(1, [ + new ContentSchedule("en-EN", DateTime.Now.AddDays(1), ContentScheduleAction.Release), // Scheduled for release + new ContentSchedule("da-DA", DateTime.Now.AddDays(-1), ContentScheduleAction.Release) // Not Scheduled for release + ]); + contentSchedules.Add(2, [ + new ContentSchedule("*", DateTime.Now.AddDays(1), ContentScheduleAction.Release) // Scheduled for release + ]); + + return contentSchedules; + } }