V16 - Introducing signs to variants (#20053)

* Adding signs to variants and adjusting HasPendingChangesSignProvider.cs

* HasPendingChangesSignProvider.cs now populates variants & refactoring to move logic to DocumentPresentationFactory.cs

* Working HasScheduleSignProvider.cs to provide variant signs

* Refactoring ISignProvider.cs to take an IEnumerable again

* Moving code from controllers to factories

* Refactoring HasPendingChangesSignProvider.cs to use the right Interface method

* Refactoring HasScheduleSignProvider.cs to be less bloated, and more readable (hopefully)

* Refactoring tests to look at variants and include a list

* Changing instantiation to be better

* Fixed minor logic issue in HasScheduleSignProvider.cs

* Refactoring to include just 1 database call.

* Adjusting tests to use the new methods.

* Reverted breaking changes
This commit is contained in:
Nicklas Kramer
2025-09-09 10:31:10 +02:00
committed by GitHub
parent 955f0c8d63
commit d51561b202
18 changed files with 386 additions and 184 deletions

View File

@@ -121,15 +121,4 @@ public abstract class ContentCollectionControllerBase<TContent, TCollectionRespo
StatusCode = StatusCodes.Status500InternalServerError,
},
});
/// <summary>
/// Populates the signs for the collection response models.
/// </summary>
protected async Task PopulateSigns(IEnumerable<TCollectionResponseModel> itemViewModels)
{
foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns<TCollectionResponseModel>()))
{
await signProvider.PopulateSignsAsync(itemViewModels);
}
}
}

View File

@@ -85,7 +85,6 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas
}
List<DocumentCollectionResponseModel> collectionResponseModels = await _documentCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!);
await PopulateSigns(collectionResponseModels);
return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total);
}
}

View File

@@ -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<SignProviderCollection>())
{
}
[HttpGet]
@@ -55,15 +44,6 @@ public class ItemDocumentItemController : DocumentItemControllerBase
.OfType<IDocumentEntitySlim>();
IEnumerable<DocumentItemResponseModel> responseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel);
await PopulateSigns(responseModels);
return Ok(responseModels);
}
private async Task PopulateSigns(IEnumerable<DocumentItemResponseModel> itemViewModels)
{
foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns<DocumentItemResponseModel>()))
{
await signProvider.PopulateSignsAsync(itemViewModels);
}
}
}

View File

@@ -84,7 +84,6 @@ public class ByKeyMediaCollectionController : MediaCollectionControllerBase
}
List<MediaCollectionResponseModel> collectionResponseModels = await _mediaCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!);
await PopulateSigns(collectionResponseModels);
return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total);
}
}

View File

@@ -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<TContent, TCollection
where TValueResponseModelBase : ValueResponseModelBase
where TVariantResponseModel : VariantResponseModelBase
{
private readonly SignProviderCollection _signProviderCollection;
private readonly IUmbracoMapper _mapper;
protected ContentCollectionPresentationFactory(IUmbracoMapper mapper) => _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<SignProviderCollection>())
{
}
protected ContentCollectionPresentationFactory(
IUmbracoMapper mapper,
SignProviderCollection signProviderCollection)
{
_mapper = mapper;
_signProviderCollection = signProviderCollection;
}
public async Task<List<TCollectionResponseModel>> CreateCollectionModelAsync(ListViewPagedModel<TContent> contentCollection)
{
@@ -36,8 +54,19 @@ public abstract class ContentCollectionPresentationFactory<TContent, TCollection
await SetUnmappedProperties(contentCollection, collectionResponseModels);
await PopulateSigns(collectionResponseModels);
return collectionResponseModels;
}
protected virtual Task SetUnmappedProperties(ListViewPagedModel<TContent> contentCollection, List<TCollectionResponseModel> collectionResponseModels) => Task.CompletedTask;
private async Task PopulateSigns(IEnumerable<TCollectionResponseModel> models)
{
foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns<TCollectionResponseModel>()))
{
await signProvider.PopulateSignsAsync(models);
}
}
}

View File

@@ -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<SignProviderCollection>())
{
}
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<string, string> 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<DocumentItemResponseModel>()))
{
signProvider.PopulateSignsAsync([model]).GetAwaiter().GetResult();
}
}
private void PopulateSignsOnVariants(DocumentVariantItemResponseModel model)
{
foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns<DocumentVariantItemResponseModel>()))
{
signProvider.PopulateSignsAsync([model]).GetAwaiter().GetResult();
}
}
}

View File

@@ -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
/// <inheritdoc/>
public bool CanProvideSigns<TItem>()
where TItem : IHasSigns =>
typeof(TItem) == typeof(DocumentTreeItemResponseModel) ||
typeof(TItem) == typeof(DocumentCollectionResponseModel) ||
typeof(TItem) == typeof(DocumentItemResponseModel);
typeof(TItem) == typeof(DocumentVariantItemResponseModel) ||
typeof(TItem) == typeof(DocumentVariantResponseModel);
/// <inheritdoc/>
public Task PopulateSignsAsync<TItem>(IEnumerable<TItem> itemViewModels)
public Task PopulateSignsAsync<TItem>(IEnumerable<TItem> 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
/// <summary>
/// Determines if the given item has any variant that has pending changes.
/// </summary>
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,
};
}

View File

@@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="HasScheduleSignProvider"/> class.
/// </summary>
public HasScheduleSignProvider(IContentService contentService) => _contentService = contentService;
public HasScheduleSignProvider(IContentService contentService, IIdKeyMap keyMap)
{
_contentService = contentService;
_keyMap = keyMap;
}
/// <inheritdoc/>
public bool CanProvideSigns<TItem>()
@@ -29,15 +37,89 @@ internal class HasScheduleSignProvider : ISignProvider
typeof(TItem) == typeof(DocumentItemResponseModel);
/// <inheritdoc/>
public Task PopulateSignsAsync<TItem>(IEnumerable<TItem> itemViewModels)
public Task PopulateSignsAsync<TItem>(IEnumerable<TItem> items)
where TItem : IHasSigns
{
IEnumerable<Guid> contentKeysScheduledForPublishing = _contentService.GetScheduledContentKeys(itemViewModels.Select(x => x.Id));
foreach (Guid key in contentKeysScheduledForPublishing)
IDictionary<int, IEnumerable<ContentSchedule>> schedules = _contentService.GetContentSchedulesByIds(items.Select(x => x.Id).ToArray());
foreach (TItem item in items)
{
itemViewModels.First(x => x.Id == key).AddSign(Alias);
Attempt<int> itemId = _keyMap.GetIdForKey(item.Id, UmbracoObjectTypes.Document);
if (itemId.Success is false)
{
continue;
}
if (!schedules.TryGetValue(itemId.Result, out IEnumerable<ContentSchedule>? 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<DocumentVariantItemResponseModel> PopulateVariants(
IEnumerable<DocumentVariantItemResponseModel> variants, IEnumerable<ContentSchedule> 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<DocumentVariantResponseModel> PopulateVariants(
IEnumerable<DocumentVariantResponseModel> variants, IEnumerable<ContentSchedule> 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;
}
}

View File

@@ -18,7 +18,6 @@ public interface ISignProvider
/// Populates the provided item view models with signs.
/// </summary>
/// <typeparam name="TItem">Type of item view model supporting signs.</typeparam>
/// <param name="itemViewModels">The collection of item view models to be populated with signs.</param>
Task PopulateSignsAsync<TItem>(IEnumerable<TItem> itemViewModels)
Task PopulateSignsAsync<TItem>(IEnumerable<TItem> items)
where TItem : IHasSigns;
}

View File

@@ -2,7 +2,25 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentVariantItemResponseModel : VariantItemResponseModelBase
public class DocumentVariantItemResponseModel : VariantItemResponseModelBase, IHasSigns
{
private readonly List<SignModel> _signs = [];
public Guid Id { get; }
public IEnumerable<SignModel> 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; }
}

View File

@@ -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<SignModel> _signs = [];
public Guid Id { get; }
public IEnumerable<SignModel> 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);
}

View File

@@ -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<int, IContent>, IReadR
/// <summary>
/// Gets the content keys from the provided collection of keys that are scheduled for publishing.
/// </summary>
/// <param name="keys">The content keys.</param>
/// <param name="documentIds">The IDs of the documents.</param>
/// <returns>
/// The provided collection of content keys filtered for those that are scheduled for publishing.
/// </returns>
IEnumerable<Guid> GetScheduledContentKeys(Guid[] keys) => [];
IDictionary<int, IEnumerable<ContentSchedule>> GetContentSchedulesByIds(int[] documentIds) => ImmutableDictionary<int, IEnumerable<ContentSchedule>>.Empty;
/// <summary>
/// Get the count of published items

View File

@@ -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
/// <inheritdoc/>
public IEnumerable<Guid> GetScheduledContentKeys(IEnumerable<Guid> keys)
public IDictionary<int, IEnumerable<ContentSchedule>> GetContentSchedulesByIds(Guid[] keys)
{
Guid[] idsA = keys.ToArray();
if (idsA.Length == 0)
if (keys.Length == 0)
{
return Enumerable.Empty<Guid>();
return ImmutableDictionary<int, IEnumerable<ContentSchedule>>.Empty;
}
List<int> contentIds = [];
foreach (var key in keys)
{
Attempt<int> 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());
}
}

View File

@@ -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<IContent>
bool HasChildren(int id);
/// <summary>
/// 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.
/// </summary>
/// <param name="keys">The content keys.</param>
/// <returns>
/// 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.
/// </returns>
IEnumerable<Guid> GetScheduledContentKeys(IEnumerable<Guid> keys) => [];
IDictionary<int, IEnumerable<ContentSchedule>> GetContentSchedulesByIds(Guid[] keys) => ImmutableDictionary<int, IEnumerable<ContentSchedule>>.Empty;
#endregion

View File

@@ -1672,24 +1672,30 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
}
/// <inheritdoc />
public IEnumerable<Guid> GetScheduledContentKeys(Guid[] keys)
public IDictionary<int, IEnumerable<ContentSchedule>> GetContentSchedulesByIds(int[] documentIds)
{
var action = ContentScheduleAction.Release.ToString();
DateTime now = DateTime.UtcNow;
Sql<ISqlContext> sql = Sql()
.Select<ContentScheduleDto>()
.From<ContentScheduleDto>()
.WhereIn<ContentScheduleDto>(contentScheduleDto => contentScheduleDto.NodeId, documentIds);
Sql<ISqlContext> sql = SqlContext.Sql();
sql
.Select<NodeDto>(x => x.UniqueId)
.From<DocumentDto>()
.InnerJoin<ContentDto>().On<DocumentDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<NodeDto>().On<ContentDto, NodeDto>(left => left.NodeId, right => right.NodeId)
.WhereIn<NodeDto>(x => x.UniqueId, keys)
.WhereIn<NodeDto>(x => x.NodeId, Sql()
.Select<ContentScheduleDto>(x => x.NodeId)
.From<ContentScheduleDto>()
.Where<ContentScheduleDto>(x => x.Action == action && x.Date >= now));
List<ContentScheduleDto>? contentScheduleDtos = Database.Fetch<ContentScheduleDto>(sql);
return Database.Fetch<Guid>(sql);
IDictionary<int, IEnumerable<ContentSchedule>> 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;
}
/// <inheritdoc />

View File

@@ -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]

View File

@@ -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<DocumentTreeItemResponseModel>());
Assert.IsTrue(sut.CanProvideSigns<DocumentVariantItemResponseModel>());
}
[Test]
public void HasPendingChangesSignProvider_Can_Provide_Document_Collection_Signs()
public void HasPendingChangesSignProvider_Can_Provide_Variant_Signs()
{
var sut = new HasPendingChangesSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentCollectionResponseModel>());
Assert.IsTrue(sut.CanProvideSigns<DocumentVariantResponseModel>());
}
[Test]
public void HasPendingChangesSignProvider_Can_Provide_Document_Item_Signs()
{
var sut = new HasPendingChangesSignProvider();
Assert.IsTrue(sut.CanProvideSigns<DocumentItemResponseModel>());
}
[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<DocumentTreeItemResponseModel>
var variants = new List<DocumentVariantItemResponseModel>
{
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<DocumentCollectionResponseModel>
var variants = new List<DocumentVariantResponseModel>
{
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<DocumentItemResponseModel>
{
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);
}
}

View File

@@ -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<IContentService>();
var idKeyMapMock = new Mock<IIdKeyMap>();
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object);
Assert.IsTrue(sut.CanProvideSigns<DocumentTreeItemResponseModel>());
}
@@ -25,8 +30,9 @@ internal class HasScheduleSignProviderTests
public void HasScheduleSignProvider_Can_Provide_Document_Collection_Signs()
{
var contentServiceMock = new Mock<IContentService>();
var idKeyMapMock = new Mock<IIdKeyMap>();
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object);
Assert.IsTrue(sut.CanProvideSigns<DocumentCollectionResponseModel>());
}
@@ -34,8 +40,9 @@ internal class HasScheduleSignProviderTests
public void HasScheduleSignProvider_Can_Provide_Document_Item_Signs()
{
var contentServiceMock = new Mock<IContentService>();
var idKeyMapMock = new Mock<IIdKeyMap>();
var sut = new HasScheduleSignProvider(contentServiceMock.Object);
var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object);
Assert.IsTrue(sut.CanProvideSigns<DocumentItemResponseModel>());
}
@@ -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<IIdKeyMap>();
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<IContentService>();
contentServiceMock
.Setup(x => x.GetScheduledContentKeys(It.IsAny<IEnumerable<Guid>>()))
.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<DocumentTreeItemResponseModel>
{
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<IIdKeyMap>();
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<IContentService>();
contentServiceMock
.Setup(x => x.GetScheduledContentKeys(It.IsAny<IEnumerable<Guid>>()))
.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<DocumentCollectionResponseModel>
{
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<IIdKeyMap>();
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<IContentService>();
contentServiceMock
.Setup(x => x.GetScheduledContentKeys(It.IsAny<IEnumerable<Guid>>()))
.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<DocumentItemResponseModel>
{
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<int, IEnumerable<ContentSchedule>> CreateContentSchedules()
{
Dictionary<int, IEnumerable<ContentSchedule>> contentSchedules = new Dictionary<int, IEnumerable<ContentSchedule>>();
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;
}
}