diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs
index a38ade606019..e41f16cee62f 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 @@ protected IActionResult ContentCollectionOperationStatusResult(ContentCollection
StatusCode = StatusCodes.Status500InternalServerError,
},
});
-
- ///
- /// 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 5ab89ba2748c..83ef32972c51 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 async Task ByKey(
}
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 f67f880e57d0..d65183c82423 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 async Task Item(
.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 184cfe4de036..7029e9311d36 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 async Task ByKey(
}
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 0607df51084c..f151d6337bf4 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 async Task> CreateCollectionModelAsync(Lis
await SetUnmappedProperties(contentCollection, collectionResponseModels);
+
+ await PopulateSigns(collectionResponseModels);
+
return collectionResponseModels;
}
protected virtual Task SetUnmappedProperties(ListViewPagedModel 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 f0e3c9901e62..37c127a24079 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 @@ public DocumentPresentationFactory(
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 @@ public DocumentPresentationFactory(
_publicAccessService = publicAccessService;
_timeProvider = timeProvider;
_idKeyMap = idKeyMap;
+ _signProviderCollection = signProviderCollection;
}
[Obsolete("Schedule for removal in v17")]
@@ -105,6 +130,8 @@ public DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim ent
responseModel.Variants = CreateVariantsItemResponseModels(entity);
+ PopulateSignsOnDocuments(responseModel);
+
return responseModel;
}
@@ -125,23 +152,29 @@ public IEnumerable CreateVariantsItemResponseM
{
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 @@ public Attempt, ContentPublishingOperationStat
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 50c421a792e1..e7563aa31071 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 Task PopulateSignsAsync(IEnumerable itemViewModels)
///
/// 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 599d10ae67e5..e9f949a57ac8 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 @@ public bool CanProvideSigns()
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 e0324f05c875..e47a128e741a 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 @@ bool CanProvideSigns()
/// 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 ccdc16ec0d17..3e4d8913f8a5 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 b6990c1b3c71..cfa3ef58096e 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 @@
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 9ff75c2b3d11..6ac6470a8575 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 016d4a83caf5..5d4ed2f98ca8 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 IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pag
///
- public IEnumerable GetScheduledContentKeys(IEnumerable keys)
+ public IDictionary> GetContentSchedulesByIds(Guid[] keys)
{
- Guid[] idsA = keys.ToArray();
- if (idsA.Length == 0)
+ if (keys.Length == 0)
+ {
+ return ImmutableDictionary>.Empty;
+ }
+
+ List contentIds = [];
+ foreach (var key in keys)
{
- return Enumerable.Empty();
+ 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 228d7fa6dbea..280cf1847a5a 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 @@ IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId
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 1279d621860b..6d5756632140 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -1672,24 +1672,30 @@ public IEnumerable GetContentForRelease(DateTime date)
}
///
- public IEnumerable GetScheduledContentKeys(Guid[] keys)
+ public IDictionary> GetContentSchedulesByIds(int[] documentIds)
{
- var action = ContentScheduleAction.Release.ToString();
- DateTime now = DateTime.UtcNow;
-
- 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));
-
- return Database.Fetch(sql);
+ Sql sql = Sql()
+ .Select()
+ .From()
+ .WhereIn(contentScheduleDto => contentScheduleDto.NodeId, documentIds);
+
+ List? contentScheduleDtos = 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 0271b77e8482..e19221638a90 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 @@ public void Can_Get_Content_For_Expiration()
}
[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 @@ public void Can_Get_Scheduled_Content_Keys()
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 7bc743591961..58dcb07c3203 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",
},
- };
-
- await sut.PopulateSignsAsync(viewModels);
-
- Assert.AreEqual(viewModels[0].Signs.Count(), 0);
- Assert.AreEqual(viewModels[1].Signs.Count(), 1);
-
- var signModel = viewModels[1].Signs.First();
- Assert.AreEqual("Umb.PendingChanges", signModel.Alias);
- }
-
- [Test]
- public async Task HasPendingChangesSignProvider_Should_Populate_Document_Collection_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",
- },
- ],
+ 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_Item_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();
+ 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 48b97ff30b76..7292b3a9ae18 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 @@ public void HasScheduleSignProvider_Can_Provide_Document_Tree_Signs()
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 @@ public void HasScheduleSignProvider_Can_Provide_Document_Collection_Signs()
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 @@ public async Task HasScheduleSignProvider_Should_Populate_Document_Tree_Signs()
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 @@ public async Task HasScheduleSignProvider_Should_Populate_Document_Collection_Si
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 @@ public async Task HasScheduleSignProvider_Should_Populate_Document_Item_Signs()
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;
+ }
}