diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index daca62926a..b9d1c0b7b4 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -141,7 +141,7 @@ namespace Umbraco.Extensions /// /// Gets the current status of the Content /// - public static ContentStatus GetStatus(this IContent content, string culture = null) + public static ContentStatus GetStatus(this IContent content, ContentScheduleCollection contentSchedule, string culture = null) { if (content.Trashed) return ContentStatus.Trashed; @@ -151,11 +151,11 @@ namespace Umbraco.Extensions else if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); - var expires = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Expire); + var expires = contentSchedule.GetSchedule(culture, ContentScheduleAction.Expire); if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) return ContentStatus.Expired; - var release = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Release); + var release = contentSchedule.GetSchedule(culture, ContentScheduleAction.Release); if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) return ContentStatus.AwaitingRelease; diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 276219aae3..3648f7907e 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -15,7 +15,6 @@ namespace Umbraco.Cms.Core.Models public class Content : ContentBase, IContent { private int? _templateId; - private ContentScheduleCollection _schedule; private bool _published; private PublishedState _publishedState; private HashSet _editedCultures; @@ -112,44 +111,6 @@ namespace Umbraco.Cms.Core.Models PublishedVersionId = 0; } - /// - [DoNotClone] - public ContentScheduleCollection ContentSchedule - { - get - { - if (_schedule == null) - { - _schedule = new ContentScheduleCollection(); - _schedule.CollectionChanged += ScheduleCollectionChanged; - } - return _schedule; - } - set - { - if (_schedule != null) - { - _schedule.ClearCollectionChangedEvents(); - } - - SetPropertyValueAndDetectChanges(value, ref _schedule, nameof(ContentSchedule)); - if (_schedule != null) - { - _schedule.CollectionChanged += ScheduleCollectionChanged; - } - } - } - - /// - /// Collection changed event handler to ensure the schedule field is set to dirty when the schedule changes - /// - /// - /// - private void ScheduleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(ContentSchedule)); - } - /// /// Gets or sets the template used by the Content. /// This is used to override the default one from the ContentType. @@ -484,14 +445,6 @@ namespace Umbraco.Cms.Core.Models clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler } - //if properties exist then deal with event bindings - if (clonedContent._schedule != null) - { - clonedContent._schedule.ClearCollectionChangedEvents(); //clear this event handler if any - clonedContent._schedule = (ContentScheduleCollection)_schedule.DeepClone(); //manually deep clone - clonedContent._schedule.CollectionChanged += clonedContent.ScheduleCollectionChanged; //re-assign correct event handler - } - clonedContent._currentPublishCultureChanges.updatedCultures = null; clonedContent._currentPublishCultureChanges.addedCultures = null; clonedContent._currentPublishCultureChanges.removedCultures = null; diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs index 7a3f441e0a..23d5fbc2fd 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; @@ -7,18 +7,23 @@ using Umbraco.Cms.Core.Routing; namespace Umbraco.Cms.Core.Models.ContentEditing { + public class ContentItemDisplay : ContentItemDisplay { } + + public class ContentItemDisplayWithSchedule : ContentItemDisplay { } + /// /// A model representing a content item to be displayed in the back office /// [DataContract(Name = "content", Namespace = "")] - public class ContentItemDisplay : INotificationModel, IErrorModel //ListViewAwareContentItemDisplayBase + public class ContentItemDisplay : INotificationModel, IErrorModel //ListViewAwareContentItemDisplayBase + where TVariant : ContentVariantDisplay { public ContentItemDisplay() { AllowPreview = true; Notifications = new List(); Errors = new Dictionary(); - Variants = new List(); + Variants = new List(); ContentApps = new List(); } @@ -60,7 +65,7 @@ namespace Umbraco.Cms.Core.Models.ContentEditing /// If a content item is invariant, this collection will only contain one item, else it will contain all culture variants /// [DataMember(Name = "variants")] - public IEnumerable Variants { get; set; } + public IEnumerable Variants { get; set; } [DataMember(Name = "owner")] public UserProfile Owner { get; set; } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs index b1d53c2059..70e025a865 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -60,12 +60,6 @@ namespace Umbraco.Cms.Core.Models.ContentEditing [DataMember(Name = "publishDate")] public DateTime? PublishDate { get; set; } - [DataMember(Name = "releaseDate")] - public DateTime? ReleaseDate { get; set; } - - [DataMember(Name = "expireDate")] - public DateTime? ExpireDate { get; set; } - /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// @@ -76,4 +70,13 @@ namespace Umbraco.Cms.Core.Models.ContentEditing [ReadOnly(true)] public List Notifications { get; private set; } } + + public class ContentVariantScheduleDisplay : ContentVariantDisplay + { + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } + + [DataMember(Name = "expireDate")] + public DateTime? ExpireDate { get; set; } + } } diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index 34e1dcea3f..f8f6c323f7 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -224,5 +224,19 @@ namespace Umbraco.Cms.Core.Models return true; } + + public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) + { + var schedule = new ContentScheduleCollection(); + schedule.Add(string.Empty, release, expire); + return schedule; + } + + public static ContentScheduleCollection CreateWithEntry(string culture, DateTime? release, DateTime? expire) + { + var schedule = new ContentScheduleCollection(); + schedule.Add(culture, release, expire); + return schedule; + } } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 516d82b7bb..4951741122 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Umbraco.Cms.Core.Models @@ -12,11 +12,6 @@ namespace Umbraco.Cms.Core.Models /// public interface IContent : IContentBase { - /// - /// Gets or sets the content schedule - /// - ContentScheduleCollection ContentSchedule { get; set; } - /// /// Gets or sets the template id used to render the content. /// diff --git a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs index 8aaa515dcd..d735757508 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Mapping; @@ -19,24 +19,24 @@ namespace Umbraco.Cms.Core.Models.Mapping _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } - public IEnumerable Map(IContent source, MapperContext context) + public IEnumerable Map(IContent source, MapperContext context) where TVariant : ContentVariantDisplay { var variesByCulture = source.ContentType.VariesByCulture(); var variesBySegment = source.ContentType.VariesBySegment(); - IList variants = new List(); + IList variants = new List(); if (!variesByCulture && !variesBySegment) { // this is invariant so just map the IContent instance to ContentVariationDisplay - var variantDisplay = context.Map(source); + var variantDisplay = context.Map(source); variants.Add(variantDisplay); } else if (variesByCulture && !variesBySegment) { var languages = GetLanguages(context); variants = languages - .Select(language => CreateVariantDisplay(context, source, language, null)) + .Select(language => CreateVariantDisplay(context, source, language, null)) .ToList(); } else if (variesBySegment && !variesByCulture) @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Core.Models.Mapping // Segment only var segments = GetSegments(source); variants = segments - .Select(segment => CreateVariantDisplay(context, source, null, segment)) + .Select(segment => CreateVariantDisplay(context, source, null, segment)) .ToList(); } else @@ -61,14 +61,16 @@ namespace Umbraco.Cms.Core.Models.Mapping variants = languages .SelectMany(language => segments - .Select(segment => CreateVariantDisplay(context, source, language, segment))) + .Select(segment => CreateVariantDisplay(context, source, language, segment))) .ToList(); } return SortVariants(variants); } - private IList SortVariants(IList variants) + + + private IList SortVariants(IList variants) where TVariant : ContentVariantDisplay { if (variants == null || variants.Count <= 1) { @@ -128,12 +130,12 @@ namespace Umbraco.Cms.Core.Models.Mapping return segments.Distinct(); } - private ContentVariantDisplay CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language language, string segment) + private TVariant CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language language, string segment) where TVariant : ContentVariantDisplay { context.SetCulture(language?.IsoCode); context.SetSegment(segment); - var variantDisplay = context.Map(content); + var variantDisplay = context.Map(content); variantDisplay.Segment = segment; variantDisplay.Language = language; diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index e58664fef8..28b1eb8636 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -67,6 +67,20 @@ namespace Umbraco.Cms.Core.Services /// IContent GetById(Guid key); + /// + /// Gets publish/unpublish schedule for a content node. + /// + /// Id of the Content to load schedule for + /// + ContentScheduleCollection GetContentScheduleByContentId(int contentId); + + /// + /// Persists publish/unpublish schedule for a content node. + /// + /// + /// + void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule); + /// /// Gets documents. /// @@ -236,7 +250,7 @@ namespace Umbraco.Cms.Core.Services /// /// Saves a document. /// - OperationResult Save(IContent content, int userId = Constants.Security.SuperUserId); + OperationResult Save(IContent content, int userId = Constants.Security.SuperUserId, ContentScheduleCollection contentSchedule = null); /// /// Saves documents. diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs index 749633a1f3..a451ac0879 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs @@ -172,9 +172,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories return dto; } - public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ILanguageRepository languageRepository) + public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ContentScheduleCollection contentSchedule, ILanguageRepository languageRepository) { - return entity.ContentSchedule.FullSchedule.Select(x => + return contentSchedule.FullSchedule.Select(x => (x, new ContentScheduleDto { Action = x.Action.ToString(), diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentRepository.cs index 03d5fd12e2..0aebfa28a5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -7,6 +7,20 @@ namespace Umbraco.Cms.Core.Persistence.Repositories { public interface IDocumentRepository : IContentRepository, IReadRepository { + /// + /// Gets publish/unpublish schedule for a content node. + /// + /// + /// + ContentScheduleCollection GetContentSchedule(int contentId); + + /// + /// Persists publish/unpublish schedule for a content node. + /// + /// + /// + void PersistContentSchedule(IContent content, ContentScheduleCollection schedule); + /// /// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date. /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 22e2480bad..af98cd7f10 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -294,7 +294,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return MapDtosToContent(Database.Fetch(sql), true, // load bare minimum, need variants though since this is used to rollback with variants - false, false, false, true).Skip(skip).Take(take); + false, false, true).Skip(skip).Take(take); } public override IContent GetVersion(int versionId) @@ -477,9 +477,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); - //insert the schedule - PersistContentSchedule(entity, false); - // persist the variations if (entity.ContentType.VariesByCulture()) { @@ -735,12 +732,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Update(dto); - //update the schedule - if (entity.IsPropertyDirty(nameof(entity.ContentSchedule))) - { - PersistContentSchedule(entity, true); - } - // if entity is publishing, update tags, else leave tags there // means that implicitly unpublished, or trashed, entities *still* have tags in db if (entity.PublishedState == PublishedState.Publishing) @@ -797,19 +788,27 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement //} } - private void PersistContentSchedule(IContent content, bool update) + /// + public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) { - var schedules = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository).ToList(); + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (contentSchedule == null) + { + throw new ArgumentNullException(nameof(contentSchedule)); + } + + var schedules = ContentBaseFactory.BuildScheduleDto(content, contentSchedule, LanguageRepository).ToList(); //remove any that no longer exist - if (update) - { - var ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); - Database.Execute(Sql() - .Delete() - .Where(x => x.NodeId == content.Id) - .WhereNotIn(x => x.Id, ids)); - } + var ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); + Database.Execute(Sql() + .Delete() + .Where(x => x.NodeId == content.Id) + .WhereNotIn(x => x.Id, ids)); //add/update the rest foreach (var schedule in schedules) @@ -1208,7 +1207,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement bool withCache = false, bool loadProperties = true, bool loadTemplates = true, - bool loadSchedule = true, bool loadVariants = true) { var temps = new List>(); @@ -1283,8 +1281,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement properties = GetPropertyCollections(temps); } - var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray()); - // assign templates and properties foreach (var temp in temps) { @@ -1306,14 +1302,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement else throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); } - - if (loadSchedule) - { - // load in the schedule - if (schedule.TryGetValue(temp.Content.Id, out var s)) - temp.Content.ContentSchedule = s; - } - } if (loadVariants) @@ -1371,11 +1359,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement SetVariations(content, contentVariations, documentVariations); } - //load in the schedule - var schedule = GetContentSchedule(dto.NodeId); - if (schedule.TryGetValue(dto.NodeId, out var s)) - content.ContentSchedule = s; - // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); return content; @@ -1386,21 +1369,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } } - private IDictionary GetContentSchedule(params int[] contentIds) + /// + public ContentScheduleCollection GetContentSchedule(int contentId) { - var result = new Dictionary(); + var result = new ContentScheduleCollection(); - var scheduleDtos = Database.FetchByGroups(contentIds, 2000, batch => Sql() + var scheduleDtos = Database.Fetch(Sql() .Select() .From() - .WhereIn(x => x.NodeId, batch)); + .Where(x => x.NodeId == contentId )); foreach (var scheduleDto in scheduleDtos) { - if (!result.TryGetValue(scheduleDto.NodeId, out var col)) - col = result[scheduleDto.NodeId] = new ContentScheduleCollection(); - - col.Add(new ContentSchedule(scheduleDto.Id, + result.Add(new ContentSchedule(scheduleDto.Id, LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, scheduleDto.Date, scheduleDto.Action == ContentScheduleAction.Release.ToString() diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs index 21c365dd8e..2182531ff9 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs @@ -374,6 +374,26 @@ namespace Umbraco.Cms.Core.Services.Implement } } + /// + public ContentScheduleCollection GetContentScheduleByContentId(int contentId) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + return _documentRepository.GetContentSchedule(contentId); + } + } + + /// + public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + _documentRepository.PersistContentSchedule(content, contentSchedule); + } + } + /// /// /// @@ -757,7 +777,7 @@ namespace Umbraco.Cms.Core.Services.Implement #region Save, Publish, Unpublish /// - public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId, ContentScheduleCollection contentSchedule = null) { PublishedState publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) @@ -801,6 +821,11 @@ namespace Umbraco.Cms.Core.Services.Implement _documentRepository.Save(content); + if (contentSchedule != null) + { + _documentRepository.PersistContentSchedule(content, contentSchedule); + } + scope.Notifications.Publish(new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification)); // TODO: we had code here to FORCE that this event can never be suppressed. But that just doesn't make a ton of sense?! @@ -1431,10 +1456,11 @@ namespace Umbraco.Cms.Core.Services.Implement foreach (var d in _documentRepository.GetContentForExpiration(date)) { + ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id); if (d.ContentType.VariesByCulture()) { //find which cultures have pending schedules - var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) + var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Expire, date) .Select(x => x.Culture) .Distinct() .ToList(); @@ -1452,11 +1478,12 @@ namespace Umbraco.Cms.Core.Services.Implement foreach (var c in pendingCultures) { //Clear this schedule for this culture - d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); + contentSchedule.Clear(c, ContentScheduleAction.Expire, date); //set the culture to be published d.UnpublishCulture(c); } + _documentRepository.PersistContentSchedule(d, contentSchedule); var result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId); if (result.Success == false) _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1465,8 +1492,9 @@ namespace Umbraco.Cms.Core.Services.Implement } else { - //Clear this schedule - d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); + //Clear this schedule for this culture + contentSchedule.Clear(ContentScheduleAction.Expire, date); + _documentRepository.PersistContentSchedule(d, contentSchedule); var result = Unpublish(d, userId: d.WriterId); if (result.Success == false) _logger.LogError(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1492,10 +1520,11 @@ namespace Umbraco.Cms.Core.Services.Implement foreach (var d in _documentRepository.GetContentForRelease(date)) { + ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id); if (d.ContentType.VariesByCulture()) { //find which cultures have pending schedules - var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Release, date) + var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date) .Select(x => x.Culture) .Distinct() .ToList(); @@ -1514,9 +1543,10 @@ namespace Umbraco.Cms.Core.Services.Implement foreach (var culture in pendingCultures) { //Clear this schedule for this culture - d.ContentSchedule.Clear(culture, ContentScheduleAction.Release, date); + contentSchedule.Clear(culture, ContentScheduleAction.Release, date); - if (d.Trashed) continue; // won't publish + if (d.Trashed) + continue; // won't publish //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed IProperty[] invalidProperties = null; @@ -1527,7 +1557,8 @@ namespace Umbraco.Cms.Core.Services.Implement d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); publishing &= tryPublish; //set the culture to be published - if (!publishing) continue; // move to next document + if (!publishing) + continue; // move to next document } PublishResult result; @@ -1537,7 +1568,11 @@ namespace Umbraco.Cms.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else + { + _documentRepository.PersistContentSchedule(d, contentSchedule); result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId); + } + if (result.Success == false) _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1547,11 +1582,19 @@ namespace Umbraco.Cms.Core.Services.Implement else { //Clear this schedule - d.ContentSchedule.Clear(ContentScheduleAction.Release, date); + contentSchedule.Clear(ContentScheduleAction.Release, date); - var result = d.Trashed - ? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d) - : SaveAndPublish(d, userId: d.WriterId); + PublishResult result = null; + + if (d.Trashed) + { + result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d); + } + else + { + _documentRepository.PersistContentSchedule(d, contentSchedule); + result = SaveAndPublish(d, userId: d.WriterId); + } if (result.Success == false) _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -2638,12 +2681,13 @@ namespace Umbraco.Cms.Core.Services.Implement return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } + ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id); //loop over each culture publishing - or string.Empty for invariant foreach (var culture in culturesPublishing ?? (new[] { string.Empty })) { // ensure that the document status is correct // note: culture will be string.Empty for invariant - switch (content.GetStatus(culture)) + switch (content.GetStatus(contentSchedule, culture)) { case ContentStatus.Expired: if (!variesByCulture) @@ -2762,20 +2806,18 @@ namespace Umbraco.Cms.Core.Services.Implement { var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); - //TODO: What is this check?? we just created this attempt and of course it is Success?! - if (attempt.Success == false) - return attempt; - // if the document has any release dates set to before now, // they should be removed so they don't interrupt an unpublish // otherwise it would remain released == published - var pastReleases = content.ContentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now); + var contentSchedule = _documentRepository.GetContentSchedule(content.Id); + var pastReleases = contentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now); foreach (var p in pastReleases) - content.ContentSchedule.Remove(p); + contentSchedule.Remove(p); if (pastReleases.Count > 0) _logger.LogInformation("Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id); + _documentRepository.PersistContentSchedule(content, contentSchedule); // change state to unpublishing content.PublishedState = PublishedState.Unpublishing; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 70678545d9..c051bde3c9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -321,15 +321,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [OutgoingEditorModelEvent] [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] - public ActionResult GetById(int id) + public ActionResult GetById(int id) { var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) { return HandleContentNotFound(id); } - var content = MapToDisplay(foundContent); - return content; + + return MapToDisplayWithSchedule(foundContent); } /// @@ -339,16 +339,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [OutgoingEditorModelEvent] [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] - public ActionResult GetById(Guid id) + public ActionResult GetById(Guid id) { var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) { return HandleContentNotFound(id); } - - var content = MapToDisplay(foundContent); - return content; + return MapToDisplayWithSchedule(foundContent); } /// @@ -358,7 +356,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [OutgoingEditorModelEvent] [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] - public ActionResult GetById(Udi id) + public ActionResult GetById(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -710,11 +708,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [FileUploadCleanupFilter] [ContentSaveValidation] - public async Task> PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + public async Task>> PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { var contentItemDisplay = await PostSaveInternal( contentItem, - content => + (content, _) => { if (!EnsureUniqueName(content.Name, content, "Name")) { @@ -742,17 +740,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [FileUploadCleanupFilter] [ContentSaveValidation] [OutgoingEditorModelEvent] - public async Task> PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + public async Task>> PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { var contentItemDisplay = await PostSaveInternal( contentItem, - content => _contentService.Save(contentItem.PersistedContent, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id), - MapToDisplay); + (content, contentSchedule) => _contentService.Save(content, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id, contentSchedule), + MapToDisplayWithSchedule); return contentItemDisplay; } - private async Task> PostSaveInternal(ContentItemSave contentItem, Func saveMethod, Func mapToDisplay) + private async Task>> PostSaveInternal(ContentItemSave contentItem, Func saveMethod, Func> mapToDisplay) + where TVariant : ContentVariantDisplay { // Recent versions of IE/Edge may send in the full client side file path instead of just the file name. // To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all @@ -832,17 +831,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentSavedText", "editVariantSavedText", cultureForInvariantErrors, out wasCancelled); + SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentSavedText", "editVariantSavedText", cultureForInvariantErrors, null, out wasCancelled); break; case ContentSaveAction.Schedule: case ContentSaveAction.ScheduleNew: - - if (!SaveSchedule(contentItem, globalNotifications)) + ContentScheduleCollection contentSchedule = _contentService.GetContentScheduleByContentId(contentItem.Id); + if (!SaveSchedule(contentItem, contentSchedule, globalNotifications)) { wasCancelled = false; break; } - SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentScheduledSavedText", "editVariantSavedText", cultureForInvariantErrors, out wasCancelled); + SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentScheduledSavedText", "editVariantSavedText", cultureForInvariantErrors, contentSchedule, out wasCancelled); break; case ContentSaveAction.SendPublish: @@ -1047,12 +1046,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Method is used for normal Saving and Scheduled Publishing /// - private void SaveAndNotify(ContentItemSave contentItem, Func saveMethod, int variantCount, + private void SaveAndNotify(ContentItemSave contentItem, Func saveMethod, int variantCount, Dictionary notifications, SimpleNotificationModel globalNotifications, - string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string cultureForInvariantErrors, + string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string cultureForInvariantErrors, ContentScheduleCollection contentSchedule, out bool wasCancelled) { - var saveResult = saveMethod(contentItem.PersistedContent); + var saveResult = saveMethod(contentItem.PersistedContent, contentSchedule); wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent; if (saveResult.Success) { @@ -1087,20 +1086,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - private bool SaveSchedule(ContentItemSave contentItem, SimpleNotificationModel globalNotifications) + private bool SaveSchedule(ContentItemSave contentItem, ContentScheduleCollection contentSchedule, SimpleNotificationModel globalNotifications) { if (!contentItem.PersistedContent.ContentType.VariesByCulture()) - return SaveScheduleInvariant(contentItem, globalNotifications); + return SaveScheduleInvariant(contentItem, contentSchedule, globalNotifications); else - return SaveScheduleVariant(contentItem); + return SaveScheduleVariant(contentItem, contentSchedule); } - private bool SaveScheduleInvariant(ContentItemSave contentItem, SimpleNotificationModel globalNotifications) + private bool SaveScheduleInvariant(ContentItemSave contentItem, ContentScheduleCollection contentSchedule, SimpleNotificationModel globalNotifications) { var variant = contentItem.Variants.First(); - var currRelease = contentItem.PersistedContent.ContentSchedule.GetSchedule(ContentScheduleAction.Release).ToList(); - var currExpire = contentItem.PersistedContent.ContentSchedule.GetSchedule(ContentScheduleAction.Expire).ToList(); + var currRelease = contentSchedule.GetSchedule(ContentScheduleAction.Release).ToList(); + var currExpire = contentSchedule.GetSchedule(ContentScheduleAction.Expire).ToList(); //Do all validation of data first @@ -1137,45 +1136,41 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //remove any existing release dates so we can replace it //if there is a release date in the request or if there was previously a release and the request value is null then we are clearing the schedule if (variant.ReleaseDate.HasValue || currRelease.Count > 0) - contentItem.PersistedContent.ContentSchedule.Clear(ContentScheduleAction.Release); + contentSchedule.Clear(ContentScheduleAction.Release); //remove any existing expire dates so we can replace it //if there is an expiry date in the request or if there was a previous expiry and the request value is null then we are clearing the schedule if (variant.ExpireDate.HasValue || currExpire.Count > 0) - contentItem.PersistedContent.ContentSchedule.Clear(ContentScheduleAction.Expire); + contentSchedule.Clear(ContentScheduleAction.Expire); //add the new schedule - contentItem.PersistedContent.ContentSchedule.Add(variant.ReleaseDate, variant.ExpireDate); + contentSchedule.Add(variant.ReleaseDate, variant.ExpireDate); return true; } - private bool SaveScheduleVariant(ContentItemSave contentItem) + private bool SaveScheduleVariant(ContentItemSave contentItem, ContentScheduleCollection contentSchedule) { //All variants in this collection should have a culture if we get here but we'll double check and filter here) var cultureVariants = contentItem.Variants.Where(x => !x.Culture.IsNullOrWhiteSpace()).ToList(); var mandatoryCultures = _allLangs.Value.Values.Where(x => x.IsMandatory).Select(x => x.IsoCode).ToList(); - //Make a copy of the current schedule and apply updates to it - - var schedCopy = (ContentScheduleCollection)contentItem.PersistedContent.ContentSchedule.DeepClone(); - foreach (var variant in cultureVariants.Where(x => x.Save)) { - var currRelease = schedCopy.GetSchedule(variant.Culture, ContentScheduleAction.Release).ToList(); - var currExpire = schedCopy.GetSchedule(variant.Culture, ContentScheduleAction.Expire).ToList(); + var currRelease = contentSchedule.GetSchedule(variant.Culture, ContentScheduleAction.Release).ToList(); + var currExpire = contentSchedule.GetSchedule(variant.Culture, ContentScheduleAction.Expire).ToList(); //remove any existing release dates so we can replace it //if there is a release date in the request or if there was previously a release and the request value is null then we are clearing the schedule if (variant.ReleaseDate.HasValue || currRelease.Count > 0) - schedCopy.Clear(variant.Culture, ContentScheduleAction.Release); + contentSchedule.Clear(variant.Culture, ContentScheduleAction.Release); //remove any existing expire dates so we can replace it //if there is an expiry date in the request or if there was a previous expiry and the request value is null then we are clearing the schedule if (variant.ExpireDate.HasValue || currExpire.Count > 0) - schedCopy.Clear(variant.Culture, ContentScheduleAction.Expire); + contentSchedule.Clear(variant.Culture, ContentScheduleAction.Expire); //add the new schedule - schedCopy.Add(variant.Culture, variant.ReleaseDate, variant.ExpireDate); + contentSchedule.Add(variant.Culture, variant.ReleaseDate, variant.ExpireDate); } //now validate the new schedule to make sure it passes all of the rules @@ -1185,7 +1180,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //create lists of mandatory/non-mandatory states var mandatoryVariants = new List<(string culture, bool isPublished, List releaseDates)>(); var nonMandatoryVariants = new List<(string culture, bool isPublished, List releaseDates)>(); - foreach (var groupedSched in schedCopy.FullSchedule.GroupBy(x => x.Culture)) + foreach (var groupedSched in contentSchedule.FullSchedule.GroupBy(x => x.Culture)) { var isPublished = contentItem.PersistedContent.Published && contentItem.PersistedContent.IsCulturePublished(groupedSched.Key); var releaseDates = groupedSched.Where(x => x.Action == ContentScheduleAction.Release).Select(x => x.Date).ToList(); @@ -1218,7 +1213,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - if (!isValid) return false; + if (!isValid) + return false; //now we can validate the more basic rules for individual variants foreach (var variant in cultureVariants.Where(x => x.ReleaseDate.HasValue || x.ExpireDate.HasValue)) @@ -1248,11 +1244,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - if (!isValid) return false; + if (!isValid) + return false; - - //now that we are validated, we can assign the copied schedule back to the model - contentItem.PersistedContent.ContentSchedule = schedCopy; return true; } @@ -1855,7 +1849,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// The content and variants to unpublish /// [OutgoingEditorModelEvent] - public async Task> PostUnpublish(UnpublishContent model) + public async Task> PostUnpublish(UnpublishContent model) { var foundContent = _contentService.GetById(model.Id); @@ -1878,7 +1872,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //this means that the entire content item will be unpublished var unpublishResult = _contentService.Unpublish(foundContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); - var content = MapToDisplay(foundContent); + var content = MapToDisplayWithSchedule(foundContent); if (!unpublishResult.Success) { @@ -1908,7 +1902,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - var content = MapToDisplay(foundContent); + var content = MapToDisplayWithSchedule(foundContent); //check for this status and return the correct message if (results.Any(x => x.Value.Result == PublishResultType.SuccessUnpublishMandatoryCulture)) @@ -2096,7 +2090,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// This is required to wire up the validation in the save/publish dialog /// - private void HandleInvalidModelState(ContentItemDisplay display, string cultureForInvariantErrors) + private void HandleInvalidModelState(ContentItemDisplay display, string cultureForInvariantErrors) + where TVariant : ContentVariantDisplay { if (!ModelState.IsValid && display.Variants.Count() > 1) { @@ -2454,6 +2449,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers context.Items["CurrentUser"] = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; }); + private ContentItemDisplayWithSchedule MapToDisplayWithSchedule(IContent content) + { + ContentItemDisplayWithSchedule display = _umbracoMapper.Map(content, context => + { + context.Items["CurrentUser"] = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + context.Items["Schedule"] = _contentService.GetContentScheduleByContentId(content.Id); + }); + display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; + return display; + } + /// /// Used to map an instance to a and ensuring AllowPreview is set correctly. /// Also allows you to pass in an action for the mapper context where you can pass additional information on to the mapper. @@ -2463,7 +2469,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// private ContentItemDisplay MapToDisplay(IContent content, Action contextOptions) { - var display = _umbracoMapper.Map(content, contextOptions); + ContentItemDisplay display = _umbracoMapper.Map(content, contextOptions); display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; return display; } diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index e234fa1115..50dd66ea19 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -99,10 +99,14 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define((source, context) => new ContentPropertyCollectionDto(), Map); - mapper.Define((source, context) => new ContentItemDisplay(), Map); - mapper.Define((source, context) => new ContentVariantDisplay(), Map); mapper.Define>((source, context) => new ContentItemBasic(), Map); + mapper.Define((source, context) => new ContentPropertyCollectionDto(), Map); + + mapper.Define((source, context) => new ContentItemDisplay(), Map); + mapper.Define((source, context) => new ContentItemDisplayWithSchedule(), Map); + + mapper.Define((source, context) => new ContentVariantDisplay(), Map); + mapper.Define((source, context) => new ContentVariantScheduleDisplay(), Map); } // Umbraco.Code.MapAll @@ -112,7 +116,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping } // Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent - private void Map(IContent source, ContentItemDisplay target, MapperContext context) + private void Map(IContent source, ContentItemDisplay target, MapperContext context) where TVariant : ContentVariantDisplay { // Both GetActions and DetermineIsChildOfListView use parent, so get it once here // Parent might already be in context, so check there before using content service @@ -154,7 +158,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.UpdateDate = source.UpdateDate; target.Updater = _commonMapper.GetCreator(source, context); target.Urls = GetUrls(source); - target.Variants = _contentVariantMapper.Map(source, context); + target.Variants = _contentVariantMapper.Map(source, context); target.ContentDto = new ContentPropertyCollectionDto(); target.ContentDto.Properties = context.MapEnumerable(source.Properties); @@ -164,15 +168,20 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping private void Map(IContent source, ContentVariantDisplay target, MapperContext context) { target.CreateDate = source.CreateDate; - target.ExpireDate = GetScheduledDate(source, ContentScheduleAction.Expire, context); target.Name = source.Name; target.PublishDate = source.PublishDate; - target.ReleaseDate = GetScheduledDate(source, ContentScheduleAction.Release, context); target.State = _stateMapper.Map(source, context); target.Tabs = _tabsAndPropertiesMapper.Map(source, context); target.UpdateDate = source.UpdateDate; } + private void Map(IContent source, ContentVariantScheduleDisplay target, MapperContext context) + { + Map(source, (ContentVariantDisplay)target, context); + target.ReleaseDate = GetScheduledDate(source, ContentScheduleAction.Release, context); + target.ExpireDate = GetScheduledDate(source, ContentScheduleAction.Expire, context); + } + // Umbraco.Code.MapAll -Alias private void Map(IContent source, ContentItemBasic target, MapperContext context) { @@ -354,8 +363,15 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping private DateTime? GetScheduledDate(IContent source, ContentScheduleAction action, MapperContext context) { + _ = context.Items.TryGetValue("Schedule", out var untypedSchedule); + + if (untypedSchedule is not ContentScheduleCollection scheduleCollection) + { + throw new ApplicationException("GetScheduledDate requires a ContentScheduleCollection in the MapperContext for Key: Schedule"); + } + var culture = context.GetCulture() ?? string.Empty; - var schedule = source.ContentSchedule.GetSchedule(culture, action); + IEnumerable schedule = scheduleCollection.GetSchedule(culture, action); return schedule.FirstOrDefault()?.Date; // take the first, it's ordered by date } diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs index 2f217ea66b..d04d3a7298 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -39,8 +39,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 Subpage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Id); - Subpage.ContentSchedule.Add(DateTime.Now.AddMinutes(-5), null); - ContentService.Save(Subpage, 0); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(Subpage, 0, contentSchedule); // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055 Subpage2 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Id); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index ad189cc02a..a5086e7531 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -224,14 +224,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services c.Name = "name" + i; if (i % 2 == 0) { - c.ContentSchedule.Add(now.AddSeconds(5), null); // release in 5 seconds - OperationResult r = ContentService.Save(c); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(now.AddSeconds(5), null); // release in 5 seconds + OperationResult r = ContentService.Save(c, contentSchedule: contentSchedule); Assert.IsTrue(r.Success, r.Result.ToString()); } else { - c.ContentSchedule.Add(null, now.AddSeconds(5)); // expire in 5 seconds PublishResult r = ContentService.SaveAndPublish(c); + + var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, now.AddSeconds(5)); // expire in 5 seconds + ContentService.PersistContentSchedule(c, contentSchedule); + Assert.IsTrue(r.Success, r.Result.ToString()); } @@ -249,16 +252,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services if (i % 2 == 0) { - c.ContentSchedule.Add(alternatingCulture, now.AddSeconds(5), null); // release in 5 seconds - OperationResult r = ContentService.Save(c); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(alternatingCulture, now.AddSeconds(5), null); // release in 5 seconds + OperationResult r = ContentService.Save(c, contentSchedule: contentSchedule); Assert.IsTrue(r.Success, r.Result.ToString()); alternatingCulture = alternatingCulture == langFr.IsoCode ? langUk.IsoCode : langFr.IsoCode; } else { - c.ContentSchedule.Add(alternatingCulture, null, now.AddSeconds(5)); // expire in 5 seconds PublishResult r = ContentService.SaveAndPublish(c); + + var contentSchedule = ContentScheduleCollection.CreateWithEntry(alternatingCulture, null, now.AddSeconds(5)); // expire in 5 seconds + ContentService.PersistContentSchedule(c, contentSchedule); + Assert.IsTrue(r.Success, r.Result.ToString()); } @@ -307,20 +313,20 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // Act IContent content = ContentService.CreateAndSave("Test", Constants.System.Root, "umbTextpage", Constants.Security.SuperUserId); - content.ContentSchedule.Add(null, DateTime.Now.AddHours(2)); - ContentService.Save(content, Constants.Security.SuperUserId); - Assert.AreEqual(1, content.ContentSchedule.FullSchedule.Count); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddHours(2)); + ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule); + Assert.AreEqual(1, contentSchedule.FullSchedule.Count); - content = ContentService.GetById(content.Id); - IReadOnlyList sched = content.ContentSchedule.FullSchedule; + contentSchedule = ContentService.GetContentScheduleByContentId(content.Id); + IReadOnlyList sched = contentSchedule.FullSchedule; Assert.AreEqual(1, sched.Count); Assert.AreEqual(1, sched.Count(x => x.Culture == string.Empty)); - content.ContentSchedule.Clear(ContentScheduleAction.Expire); - ContentService.Save(content, Constants.Security.SuperUserId); + contentSchedule.Clear(ContentScheduleAction.Expire); + ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule); // Assert - content = ContentService.GetById(content.Id); - sched = content.ContentSchedule.FullSchedule; + contentSchedule = ContentService.GetContentScheduleByContentId(content.Id); + sched = contentSchedule.FullSchedule; Assert.AreEqual(0, sched.Count); Assert.IsTrue(ContentService.SaveAndPublish(content).Success); } @@ -646,7 +652,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent root = ContentService.GetById(Textpage.Id); ContentService.SaveAndPublish(root); IContent content = ContentService.GetById(Subpage.Id); - content.ContentSchedule.Add(null, DateTime.Now.AddSeconds(1)); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddSeconds(1)); + ContentService.PersistContentSchedule(content, contentSchedule); ContentService.SaveAndPublish(content); // Act @@ -1292,8 +1299,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { // Arrange IContent content = ContentService.GetById(Subpage.Id); // This Content expired 5min ago - content.ContentSchedule.Add(null, DateTime.Now.AddMinutes(-5)); - ContentService.Save(content); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddMinutes(-5)); + ContentService.Save(content, contentSchedule: contentSchedule); IContent parent = ContentService.GetById(Textpage.Id); PublishResult parentPublished = ContentService.SaveAndPublish(parent, userId: Constants.Security.SuperUserId); // Publish root Home node to enable publishing of 'Subpage.Id' @@ -1317,8 +1324,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Content content = ContentBuilder.CreateBasicContent(contentType); content.SetCultureName("Hello", "en-US"); - content.ContentSchedule.Add("en-US", null, DateTime.Now.AddMinutes(-5)); - ContentService.Save(content); + var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.Now.AddMinutes(-5)); + ContentService.Save(content, contentSchedule: contentSchedule); PublishResult published = ContentService.SaveAndPublish(content, "en-US", Constants.Security.SuperUserId); @@ -1332,8 +1339,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { // Arrange IContent content = ContentService.GetById(Subpage.Id); - content.ContentSchedule.Add(DateTime.Now.AddHours(2), null); - ContentService.Save(content, Constants.Security.SuperUserId); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null); + ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule); IContent parent = ContentService.GetById(Textpage.Id); PublishResult parentPublished = ContentService.SaveAndPublish(parent, userId: Constants.Security.SuperUserId); // Publish root Home node to enable publishing of 'Subpage.Id' @@ -1380,8 +1387,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services contentService.SaveAndPublish(content); content.Properties[0].SetValue("Foo", culture: string.Empty); - content.ContentSchedule.Add(DateTime.Now.AddHours(2), null); contentService.Save(content); + contentService.PersistContentSchedule(content, ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null)); // Act var result = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); @@ -1430,7 +1437,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services contentService.SaveAndPublish(content); - content.ContentSchedule.Add(DateTime.Now.AddHours(2), null); + contentService.PersistContentSchedule(content, ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null)); contentService.Save(content); // Act @@ -1458,8 +1465,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Content content = ContentBuilder.CreateBasicContent(contentType); content.SetCultureName("Hello", "en-US"); - content.ContentSchedule.Add("en-US", DateTime.Now.AddHours(2), null); - ContentService.Save(content); + var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.Now.AddHours(2), null); + ContentService.Save(content, contentSchedule: contentSchedule); PublishResult published = ContentService.SaveAndPublish(content, "en-US", Constants.Security.SuperUserId); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index ae66f65b96..4a8a933cce 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -809,8 +809,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 _subpage = ContentBuilder.CreateSimpleContent(_contentType, "Text Page 1", _textpage.Id); - _subpage.ContentSchedule.Add(DateTime.Now.AddMinutes(-5), null); - ContentService.Save(_subpage, 0); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(_subpage, 0, contentSchedule); // Create and Save Content "Text Page 2" based on "umbTextpage" -> 1055 _subpage2 = ContentBuilder.CreateSimpleContent(_contentType, "Text Page 2", _textpage.Id); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs index 718559af39..e2281c9585 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs @@ -253,8 +253,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); - //// content.ChangePublishedState(PublishedState.Published); content.SortOrder = 5; content.TemplateId = 88; content.Trashed = false; @@ -316,7 +314,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); content.SortOrder = 5; content.TemplateId = 88; content.Trashed = false; @@ -338,7 +335,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models Assert.AreEqual(clone.Key, content.Key); Assert.AreEqual(clone.Level, content.Level); Assert.AreEqual(clone.Path, content.Path); - Assert.IsTrue(clone.ContentSchedule.Equals(content.ContentSchedule)); Assert.AreEqual(clone.Published, content.Published); Assert.AreEqual(clone.PublishedState, content.PublishedState); Assert.AreEqual(clone.SortOrder, content.SortOrder); @@ -421,7 +417,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; @@ -442,7 +437,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models Assert.IsTrue(content.WasPropertyDirty(nameof(Content.Key))); Assert.IsTrue(content.WasPropertyDirty(nameof(Content.Level))); Assert.IsTrue(content.WasPropertyDirty(nameof(Content.Path))); - Assert.IsTrue(content.WasPropertyDirty(nameof(Content.ContentSchedule))); Assert.IsTrue(content.WasPropertyDirty(nameof(Content.SortOrder))); Assert.IsTrue(content.WasPropertyDirty(nameof(Content.TemplateId))); Assert.IsTrue(content.WasPropertyDirty(nameof(Content.Trashed))); @@ -492,8 +486,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); - //// content.ChangePublishedState(PublishedState.Publishing); content.SortOrder = 5; content.TemplateId = 88; content.Trashed = false;