V10: Fix sending content notification (#12597)
* Add mappers to map between ContentItemDisplay and ContentItemDisplayWithSchedule * Ensure SendingContentNotification is always sent * Add custom setup hook for UmbracoTestServerTestBase * Add test showing bug/fix * Test schedule being mapped correctly * Obsolete the old constructor * Removed TODO
This commit is contained in:
@@ -3,12 +3,15 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Dashboards;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.BackOffice.Filters
|
||||
@@ -29,20 +32,34 @@ namespace Umbraco.Cms.Web.BackOffice.Filters
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
public OutgoingEditorModelEventFilter(
|
||||
[ActivatorUtilitiesConstructor]public OutgoingEditorModelEventFilter(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IEventAggregator eventAggregator)
|
||||
{
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IEventAggregator eventAggregator,
|
||||
IUmbracoMapper mapper){
|
||||
_umbracoContextAccessor = umbracoContextAccessor
|
||||
?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor
|
||||
?? throw new ArgumentNullException(nameof(backOfficeSecurityAccessor));
|
||||
_eventAggregator = eventAggregator
|
||||
?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
}
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[Obsolete("Please use constructor that takes an IUmbracoMapper, scheduled for removal in V12")]
|
||||
public OutgoingEditorModelEventFilter(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IEventAggregator eventAggregator)
|
||||
: this(
|
||||
umbracoContextAccessor,
|
||||
backOfficeSecurityAccessor,
|
||||
eventAggregator,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUmbracoMapper>())
|
||||
{
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
@@ -72,7 +89,26 @@ namespace Umbraco.Cms.Web.BackOffice.Filters
|
||||
case ContentItemDisplay content:
|
||||
_eventAggregator.Publish(new SendingContentNotification(content, umbracoContext));
|
||||
break;
|
||||
case MediaItemDisplay media:
|
||||
case ContentItemDisplayWithSchedule contentWithSchedule:
|
||||
// This is a bit weird, since ContentItemDisplayWithSchedule was introduced later,
|
||||
// the SendingContentNotification only accepts ContentItemDisplay,
|
||||
// which means we have to map it to this before sending the notification.
|
||||
ContentItemDisplay? display = _mapper.Map<ContentItemDisplayWithSchedule, ContentItemDisplay>(contentWithSchedule);
|
||||
if (display is null)
|
||||
{
|
||||
// This will never happen.
|
||||
break;
|
||||
}
|
||||
|
||||
// Now that the display is mapped to the non-schedule one we can publish the notification.
|
||||
_eventAggregator.Publish(new SendingContentNotification(display, umbracoContext));
|
||||
|
||||
// We want the changes the handler makes to take effect.
|
||||
// So we have to map these changes back to the existing ContentItemWithSchedule.
|
||||
// To avoid losing the schedule information we add the old variants to context.
|
||||
_mapper.Map(display, contentWithSchedule, mapperContext => mapperContext.Items[nameof(contentWithSchedule.Variants)] = contentWithSchedule.Variants);
|
||||
break;
|
||||
case MediaItemDisplay media:
|
||||
_eventAggregator.Publish(new SendingMediaNotification(media, umbracoContext));
|
||||
break;
|
||||
case MemberDisplay member:
|
||||
|
||||
@@ -107,7 +107,130 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping
|
||||
|
||||
mapper.Define<IContent, ContentVariantDisplay>((source, context) => new ContentVariantDisplay(), Map);
|
||||
mapper.Define<IContent, ContentVariantScheduleDisplay>((source, context) => new ContentVariantScheduleDisplay(), Map);
|
||||
mapper.Define<ContentItemDisplayWithSchedule, ContentItemDisplay>((source, context) => new ContentItemDisplay(), Map);
|
||||
mapper.Define<ContentItemDisplay, ContentItemDisplayWithSchedule>((source, context) => new ContentItemDisplayWithSchedule(), Map);
|
||||
|
||||
mapper.Define<ContentVariantDisplay, ContentVariantScheduleDisplay>((source, context) => new ContentVariantScheduleDisplay(), Map);
|
||||
mapper.Define<ContentVariantScheduleDisplay, ContentVariantDisplay>((source, context) => new ContentVariantDisplay(), Map);
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private void Map(ContentVariantScheduleDisplay source, ContentVariantDisplay target, MapperContext context)
|
||||
{
|
||||
target.CreateDate = source.CreateDate;
|
||||
target.DisplayName = source.DisplayName;
|
||||
target.Language = source.Language;
|
||||
target.Name = source.Name;
|
||||
target.PublishDate = source.PublishDate;
|
||||
target.Segment = source.Segment;
|
||||
target.State = source.State;
|
||||
target.Tabs = source.Tabs;
|
||||
target.UpdateDate = source.UpdateDate;
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private void Map(ContentItemDisplay source, ContentItemDisplayWithSchedule target, MapperContext context)
|
||||
{
|
||||
target.AllowedActions = source.AllowedActions;
|
||||
target.AllowedTemplates = source.AllowedTemplates;
|
||||
target.AllowPreview = source.AllowPreview;
|
||||
target.ContentApps = source.ContentApps;
|
||||
target.ContentDto = source.ContentDto;
|
||||
target.ContentTypeAlias = source.ContentTypeAlias;
|
||||
target.ContentTypeId = source.ContentTypeId;
|
||||
target.ContentTypeKey = source.ContentTypeKey;
|
||||
target.ContentTypeName = source.ContentTypeName;
|
||||
target.DocumentType = source.DocumentType;
|
||||
target.Errors = source.Errors;
|
||||
target.Icon = source.Icon;
|
||||
target.Id = source.Id;
|
||||
target.IsBlueprint = source.IsBlueprint;
|
||||
target.IsChildOfListView = source.IsChildOfListView;
|
||||
target.IsContainer = source.IsContainer;
|
||||
target.IsElement = source.IsElement;
|
||||
target.Key = source.Key;
|
||||
target.Owner = source.Owner;
|
||||
target.ParentId = source.ParentId;
|
||||
target.Path = source.Path;
|
||||
target.PersistedContent = source.PersistedContent;
|
||||
target.SortOrder = source.SortOrder;
|
||||
target.TemplateAlias = source.TemplateAlias;
|
||||
target.TemplateId = source.TemplateId;
|
||||
target.Trashed = source.Trashed;
|
||||
target.TreeNodeUrl = source.TreeNodeUrl;
|
||||
target.Udi = source.Udi;
|
||||
target.UpdateDate = source.UpdateDate;
|
||||
target.Updater = source.Updater;
|
||||
target.Urls = source.Urls;
|
||||
target.Variants = context.MapEnumerable<ContentVariantDisplay, ContentVariantScheduleDisplay>(source.Variants);
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private void Map(ContentVariantDisplay source, ContentVariantScheduleDisplay target, MapperContext context)
|
||||
{
|
||||
target.CreateDate = source.CreateDate;
|
||||
target.DisplayName = source.DisplayName;
|
||||
target.Language = source.Language;
|
||||
target.Name = source.Name;
|
||||
target.PublishDate = source.PublishDate;
|
||||
target.Segment = source.Segment;
|
||||
target.State = source.State;
|
||||
target.Tabs = source.Tabs;
|
||||
target.UpdateDate = source.UpdateDate;
|
||||
|
||||
// We'll only try and map the ReleaseDate/ExpireDate if the "old" ContentVariantScheduleDisplay is in the context, otherwise we'll just skip it quietly.
|
||||
_ = context.Items.TryGetValue(nameof(ContentItemDisplayWithSchedule.Variants), out var variants);
|
||||
if (variants is IEnumerable<ContentVariantScheduleDisplay> scheduleDisplays)
|
||||
{
|
||||
ContentVariantScheduleDisplay? item = scheduleDisplays.FirstOrDefault(x => x.Language?.Id == source.Language?.Id && x.Segment == source.Segment);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
// If we can't find the old variants display, we'll just not try and map it.
|
||||
return;
|
||||
}
|
||||
|
||||
target.ReleaseDate = item.ReleaseDate;
|
||||
target.ExpireDate = item.ExpireDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private static void Map(ContentItemDisplayWithSchedule source, ContentItemDisplay target, MapperContext context)
|
||||
{
|
||||
target.AllowedActions = source.AllowedActions;
|
||||
target.AllowedTemplates = source.AllowedTemplates;
|
||||
target.AllowPreview = source.AllowPreview;
|
||||
target.ContentApps = source.ContentApps;
|
||||
target.ContentDto = source.ContentDto;
|
||||
target.ContentTypeAlias = source.ContentTypeAlias;
|
||||
target.ContentTypeId = source.ContentTypeId;
|
||||
target.ContentTypeKey = source.ContentTypeKey;
|
||||
target.ContentTypeName = source.ContentTypeName;
|
||||
target.DocumentType = source.DocumentType;
|
||||
target.Errors = source.Errors;
|
||||
target.Icon = source.Icon;
|
||||
target.Id = source.Id;
|
||||
target.IsBlueprint = source.IsBlueprint;
|
||||
target.IsChildOfListView = source.IsChildOfListView;
|
||||
target.IsContainer = source.IsContainer;
|
||||
target.IsElement = source.IsElement;
|
||||
target.Key = source.Key;
|
||||
target.Owner = source.Owner;
|
||||
target.ParentId = source.ParentId;
|
||||
target.Path = source.Path;
|
||||
target.PersistedContent = source.PersistedContent;
|
||||
target.SortOrder = source.SortOrder;
|
||||
target.TemplateAlias = source.TemplateAlias;
|
||||
target.TemplateId = source.TemplateId;
|
||||
target.Trashed = source.Trashed;
|
||||
target.TreeNodeUrl = source.TreeNodeUrl;
|
||||
target.Udi = source.Udi;
|
||||
target.UpdateDate = source.UpdateDate;
|
||||
target.Updater = source.Updater;
|
||||
target.Urls = source.Urls;
|
||||
target.Variants = source.Variants;
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private static void Map(IContent source, ContentPropertyCollectionDto target, MapperContext context)
|
||||
|
||||
@@ -42,6 +42,16 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest
|
||||
|
||||
protected WebApplicationFactory<UmbracoTestServerTestBase> Factory { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hook for altering UmbracoBuilder setup
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can also be used for registering test doubles.
|
||||
/// </remarks>
|
||||
protected virtual void CustomTestSetup(IUmbracoBuilder builder)
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
@@ -233,8 +243,9 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest
|
||||
.AddWebsite()
|
||||
.AddUmbracoSqlServerSupport()
|
||||
.AddUmbracoSqliteSupport()
|
||||
.AddTestServices(TestHelper) // This is the important one!
|
||||
.Build();
|
||||
.AddTestServices(TestHelper); // This is the important one!
|
||||
CustomTestSetup(builder);
|
||||
builder.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Integration.TestServerTest;
|
||||
using Umbraco.Cms.Web.BackOffice.Controllers;
|
||||
using Umbraco.Cms.Web.Common.Formatters;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Filters;
|
||||
|
||||
[TestFixture]
|
||||
public class OutgoingEditorModelEventFilterTests : UmbracoTestServerTestBase
|
||||
{
|
||||
private static int _messageCount;
|
||||
private static Action<SendingContentNotification> _handler;
|
||||
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.AddNotificationHandler<SendingContentNotification, FilterEventHandler>();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Reset() => ResetNotifications();
|
||||
|
||||
[Test]
|
||||
public async Task Content_Item_With_Schedule_Raises_SendingContentNotification()
|
||||
{
|
||||
IContentTypeService contentTypeService = GetRequiredService<IContentTypeService>();
|
||||
IContentService contentService = GetRequiredService<IContentService>();
|
||||
IJsonSerializer serializer = GetRequiredService<IJsonSerializer>();
|
||||
|
||||
var contentType = new ContentTypeBuilder().Build();
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var contentToRequest = new ContentBuilder()
|
||||
.WithoutIdentity()
|
||||
.WithContentType(contentType)
|
||||
.Build();
|
||||
|
||||
contentService.Save(contentToRequest);
|
||||
|
||||
_handler = notification => notification.Content.AllowPreview = false;
|
||||
|
||||
var url = PrepareApiControllerUrl<ContentController>(x => x.GetById(contentToRequest.Id));
|
||||
|
||||
HttpResponseMessage response = await Client.GetAsync(url);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var text = await response.Content.ReadAsStringAsync();
|
||||
text = text.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix);
|
||||
var display = serializer.Deserialize<ContentItemDisplayWithSchedule>(text);
|
||||
|
||||
Assert.AreEqual(1, _messageCount);
|
||||
Assert.IsNotNull(display);
|
||||
Assert.IsFalse(display.AllowPreview);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Publish_Schedule_Is_Mapped_Correctly()
|
||||
{
|
||||
const string UsIso = "en-US";
|
||||
const string DkIso = "da-DK";
|
||||
const string SweIso = "sv-SE";
|
||||
var contentTypeService = GetRequiredService<IContentTypeService>();
|
||||
var contentService = GetRequiredService<IContentService>();
|
||||
var localizationService = GetRequiredService<ILocalizationService>();
|
||||
IJsonSerializer serializer = GetRequiredService<IJsonSerializer>();
|
||||
|
||||
var contentType = new ContentTypeBuilder()
|
||||
.WithContentVariation(ContentVariation.Culture)
|
||||
.Build();
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var dkLang = new LanguageBuilder()
|
||||
.WithCultureInfo(DkIso)
|
||||
.WithIsDefault(false)
|
||||
.Build();
|
||||
|
||||
var sweLang = new LanguageBuilder()
|
||||
.WithCultureInfo(SweIso)
|
||||
.WithIsDefault(false)
|
||||
.Build();
|
||||
|
||||
localizationService.Save(dkLang);
|
||||
localizationService.Save(sweLang);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithoutIdentity()
|
||||
.WithContentType(contentType)
|
||||
.WithCultureName(UsIso, "Same Name")
|
||||
.WithCultureName(SweIso, "Same Name")
|
||||
.WithCultureName(DkIso, "Same Name")
|
||||
.Build();
|
||||
|
||||
contentService.Save(content);
|
||||
var schedule = new ContentScheduleCollection();
|
||||
|
||||
var dkReleaseDate = new DateTime(2022, 06, 22, 21, 30, 42);
|
||||
var dkExpireDate = new DateTime(2022, 07, 15, 18, 00, 00);
|
||||
|
||||
var sweReleaseDate = new DateTime(2022, 06, 23, 22, 30, 42);
|
||||
var sweExpireDate = new DateTime(2022, 07, 10, 14, 20, 00);
|
||||
schedule.Add(DkIso, dkReleaseDate, dkExpireDate);
|
||||
schedule.Add(SweIso, sweReleaseDate, sweExpireDate);
|
||||
contentService.PersistContentSchedule(content, schedule);
|
||||
|
||||
var url = PrepareApiControllerUrl<ContentController>(x => x.GetById(content.Id));
|
||||
|
||||
HttpResponseMessage response = await Client.GetAsync(url);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var text = await response.Content.ReadAsStringAsync();
|
||||
text = text.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix);
|
||||
var display = serializer.Deserialize<ContentItemDisplayWithSchedule>(text);
|
||||
|
||||
Assert.IsNotNull(display);
|
||||
Assert.AreEqual(1, _messageCount);
|
||||
|
||||
var dkVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == DkIso);
|
||||
Assert.IsNotNull(dkVariant);
|
||||
Assert.AreEqual(dkReleaseDate, dkVariant.ReleaseDate);
|
||||
Assert.AreEqual(dkExpireDate, dkVariant.ExpireDate);
|
||||
|
||||
var sweVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == SweIso);
|
||||
Assert.IsNotNull(sweVariant);
|
||||
Assert.AreEqual(sweReleaseDate, sweVariant.ReleaseDate);
|
||||
Assert.AreEqual(sweExpireDate, sweVariant.ExpireDate);
|
||||
|
||||
var usVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == UsIso);
|
||||
Assert.IsNotNull(usVariant);
|
||||
Assert.IsNull(usVariant.ReleaseDate);
|
||||
Assert.IsNull(usVariant.ExpireDate);
|
||||
}
|
||||
|
||||
private void ResetNotifications()
|
||||
{
|
||||
_messageCount = 0;
|
||||
_handler = null;
|
||||
}
|
||||
|
||||
private class FilterEventHandler : INotificationHandler<SendingContentNotification>
|
||||
{
|
||||
public void Handle(SendingContentNotification notification)
|
||||
{
|
||||
_messageCount += 1;
|
||||
_handler?.Invoke(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user