From f223024164870fc5abc1bdb21c1bcdcd104cd922 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Nov 2018 14:55:34 +1100 Subject: [PATCH] Got database and persistence models in place for scheduled publishing, now need to start writing tests, fixing tests and cleaning up fixme statements --- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../V_8_0_0/TablesForScheduledPublishing.cs | 46 ++++++ src/Umbraco.Core/Models/Content.cs | 41 +++--- src/Umbraco.Core/Models/ContentSchedule.cs | 49 +++++++ .../Models/ContentScheduleChange.cs | 11 ++ .../Models/ContentScheduleCollection.cs | 136 ++++++++++++++++++ src/Umbraco.Core/Models/IContent.cs | 16 +-- .../Persistence/Constants-DatabaseSchema.cs | 1 + .../Persistence/Dtos/ContentScheduleDto.cs | 33 +++++ .../Persistence/Dtos/DocumentDto.cs | 17 +-- .../Factories/ContentBaseFactory.cs | 44 ++++-- .../Persistence/Mappers/ContentMapper.cs | 2 - .../Implement/DocumentRepository.cs | 31 +++- .../Publishing/ScheduledPublisher.cs | 10 +- .../Services/Implement/ContentService.cs | 39 +++-- src/Umbraco.Core/Umbraco.Core.csproj | 5 + src/Umbraco.Tests/Models/ContentTests.cs | 13 +- .../Services/ContentServiceTests.cs | 10 +- .../Services/TestWithSomeContentBase.cs | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 4 +- 20 files changed, 421 insertions(+), 90 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs create mode 100644 src/Umbraco.Core/Models/ContentSchedule.cs create mode 100644 src/Umbraco.Core/Models/ContentScheduleChange.cs create mode 100644 src/Umbraco.Core/Models/ContentScheduleCollection.cs create mode 100644 src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 92c08b066b..ea40fdb707 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -142,6 +142,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); Chain("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); Chain("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + Chain("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs new file mode 100644 index 0000000000..64d0ed958c --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -0,0 +1,46 @@ +using NPoco; +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class TablesForScheduledPublishing : MigrationBase + { + public TablesForScheduledPublishing(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + //Get anything currently scheduled + var scheduleSql = new Sql() + .Select("nodeId", "releaseDate", "expireDate") + .From("umbracoDocument") + .Where("releaseDate IS NOT NULL OR expireDate IS NOT NULL"); + var schedules = Database.Dictionary (scheduleSql); + + //drop old cols + Delete.Column("cmsDocument").FromTable("releaseDate").Do(); + Delete.Column("cmsDocument").FromTable("expireDate").Do(); + //add new table + Create.Table().Do(); + + //migrate the schedule + foreach(var s in schedules) + { + var date = s.Value.releaseDate; + var action = ContentScheduleChange.Start.ToString(); + if (!date.HasValue) + { + date = s.Value.expireDate; + action = ContentScheduleChange.End.ToString(); + } + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { nodeId = s.Key, date = date.Value, action = action }) + .Do(); + } + } + } +} diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 3e5becf021..d73c948094 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Models { private IContentType _contentType; private ITemplate _template; + private ContentScheduleCollection _schedule; private bool _published; private PublishedState _publishedState; private DateTime? _releaseDate; @@ -86,11 +87,17 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.Template); public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published); - public readonly PropertyInfo ReleaseDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ReleaseDate); - public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate); + public readonly PropertyInfo ContentScheduleSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentSchedule); public readonly PropertyInfo PublishCultureInfosSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishCultureInfos); } + /// + public ContentScheduleCollection ContentSchedule + { + get => _schedule ?? (_schedule = new ContentScheduleCollection()); + set => SetPropertyValueAndDetectChanges(value, ref _schedule, Ps.Value.ContentScheduleSelector); + } + /// /// Gets or sets the template used by the Content. /// This is used to override the default one from the ContentType. @@ -100,7 +107,7 @@ namespace Umbraco.Core.Models /// the Default template from the ContentType will be returned. /// [DataMember] - public virtual ITemplate Template + public ITemplate Template { get => _template ?? _contentType.DefaultTemplate; set => SetPropertyValueAndDetectChanges(value, ref _template, Ps.Value.TemplateSelector); @@ -117,10 +124,14 @@ namespace Umbraco.Core.Models if(Trashed) return ContentStatus.Trashed; - if(ExpireDate.HasValue && ExpireDate.Value > DateTime.MinValue && DateTime.Now > ExpireDate.Value) + //fixme - deal with variants + var expires = ContentSchedule.GetSchedule(ContentScheduleChange.End).FirstOrDefault(); + if (expires != null && expires.Date > DateTime.MinValue && DateTime.Now > expires.Date) return ContentStatus.Expired; - if(ReleaseDate.HasValue && ReleaseDate.Value > DateTime.MinValue && ReleaseDate.Value > DateTime.Now) + //fixme - deal with variants + var release = ContentSchedule.GetSchedule(ContentScheduleChange.Start).FirstOrDefault(); + if (release != null && release.Date > DateTime.MinValue && release.Date > DateTime.Now) return ContentStatus.AwaitingRelease; if(Published) @@ -169,26 +180,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public bool Edited { get; internal set; } - /// - /// The date this Content should be released and thus be published - /// - [DataMember] - public DateTime? ReleaseDate - { - get => _releaseDate; - set => SetPropertyValueAndDetectChanges(value, ref _releaseDate, Ps.Value.ReleaseDateSelector); - } - - /// - /// The date this Content should expire and thus be unpublished - /// - [DataMember] - public DateTime? ExpireDate - { - get => _expireDate; - set => SetPropertyValueAndDetectChanges(value, ref _expireDate, Ps.Value.ExpireDateSelector); - } - /// /// Gets the ContentType used by this content object /// diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs new file mode 100644 index 0000000000..f137b85ee7 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentSchedule.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + + /// + /// Model for scheduled content + /// + [Serializable] + [DataContract(IsReference = true)] + public class ContentSchedule + { + public ContentSchedule(int id, string culture, DateTime date, ContentScheduleChange change) + { + Id = id; + Culture = culture; + Date = date; + Change = change; + } + + /// + /// The unique Id of the schedule item + /// + [DataMember] + public int Id { get; } + + /// + /// The culture for the schedule item + /// + /// + /// string.Empty represents invariant culture + /// + [DataMember] + public string Culture { get; } + + /// + /// The date for the schedule + /// + [DataMember] + public DateTime Date { get; } + + /// + /// The action to take for the schedule + /// + [DataMember] + public ContentScheduleChange Change { get; } + } +} diff --git a/src/Umbraco.Core/Models/ContentScheduleChange.cs b/src/Umbraco.Core/Models/ContentScheduleChange.cs new file mode 100644 index 0000000000..9196d16b61 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentScheduleChange.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Core.Models +{ + /// + /// The scheduled change types of scheduled content + /// + public enum ContentScheduleChange + { + Start, + End + } +} diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs new file mode 100644 index 0000000000..0198b5ab97 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -0,0 +1,136 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Umbraco.Core.Models +{ + public class ContentScheduleCollection + { + //underlying storage for the collection backed by a sorted list so that the schedule is always in order of date + private readonly Dictionary> _schedule = new Dictionary>(); + + /// + /// Add an existing schedule + /// + /// + public void Add(ContentSchedule schedule) + { + if (!_schedule.TryGetValue(schedule.Culture, out var changes)) + { + changes = new SortedList(); + _schedule[schedule.Culture] = changes; + } + + //TODO: Below will throw if there are duplicate dates added, validate/return bool? + changes.Add(schedule.Date, schedule); + } + + /// + /// Adds a new schedule for invariant content + /// + /// + /// + public void Add(DateTime? releaseDate, DateTime? expireDate) + { + Add(string.Empty, releaseDate, expireDate); + } + + /// + /// Adds a new schedule for a culture + /// + /// + /// + /// + public void Add(string culture, DateTime? releaseDate, DateTime? expireDate) + { + if (culture == null) throw new ArgumentNullException(nameof(culture)); + if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) + throw new InvalidOperationException($"The {nameof(releaseDate)} must be less than {nameof(expireDate)}"); + + if (!releaseDate.HasValue && !expireDate.HasValue) return; + + //TODO: Do we allow passing in a release or expiry date that is before now? + + if (!_schedule.TryGetValue(culture, out var changes)) + { + changes = new SortedList(); + _schedule[culture] = changes; + } + + //TODO: Below will throw if there are duplicate dates added, should validate/return bool? + // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? + + if (releaseDate.HasValue) + changes.Add(releaseDate.Value, new ContentSchedule(0, culture, releaseDate.Value, ContentScheduleChange.Start)); + if (expireDate.HasValue) + changes.Add(expireDate.Value, new ContentSchedule(0, culture, expireDate.Value, ContentScheduleChange.End)); + + } + + /// + /// Remove a scheduled change + /// + /// + public void Remove(ContentSchedule change) + { + if (_schedule.TryGetValue(change.Culture, out var s)) + { + s.Remove(change.Date); + } + } + + /// + /// Clear all of the scheduled change type for invariant content + /// + /// + public void Clear(ContentScheduleChange changeType) + { + Clear(string.Empty, changeType); + } + + /// + /// Clear all of the scheduled change type for the culture + /// + /// + /// + public void Clear(string culture, ContentScheduleChange changeType) + { + if (_schedule.TryGetValue(culture, out var s)) + { + foreach (var ofChange in s.Where(x => x.Value.Change == changeType).ToList()) + s.Remove(ofChange.Value.Date); + } + } + + /// + /// Gets the schedule for invariant content + /// + /// + public IEnumerable GetSchedule(ContentScheduleChange? changeType = null) + { + return GetSchedule(string.Empty, changeType); + } + + /// + /// Gets the schedule for a culture + /// + /// + /// + public IEnumerable GetSchedule(string culture, ContentScheduleChange? changeType = null) + { + if (_schedule.TryGetValue(culture, out var changes)) + return changeType == null ? changes.Values : changes.Values.Where(x => x.Change == changeType.Value); + return null; + } + + /// + /// Returns all schedules for both invariant and variant cultures + /// + /// + public IReadOnlyDictionary> GetFullSchedule() + { + return _schedule.ToDictionary(x => x.Key, x => (IEnumerable)x.Value.Values); + } + } +} diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 0c0d9449e0..20590a8d36 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Models { + /// /// Represents a document. /// @@ -11,6 +12,11 @@ namespace Umbraco.Core.Models /// public interface IContent : IContentBase { + /// + /// Gets or sets the content schedule + /// + ContentScheduleCollection ContentSchedule { get; set; } + /// /// Gets or sets the template used to render the content. /// @@ -60,16 +66,6 @@ namespace Umbraco.Core.Models /// DateTime? PublishDate { get; } - /// - /// Gets or sets the date and time the content item should be published. - /// - DateTime? ReleaseDate { get; set; } - - /// - /// Gets or sets the date and time the content should be unpublished. - /// - DateTime? ExpireDate { get; set; } - /// /// Gets the content type of this content. /// diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 523d09c1f7..9eb4c3f90f 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -31,6 +31,7 @@ namespace Umbraco.Core public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; public const string MediaVersion = TableNamePrefix + "MediaVersion"; + public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType"; public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs new file mode 100644 index 0000000000..9f77ac8d02 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs @@ -0,0 +1,33 @@ +using System; +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Persistence.Dtos +{ + [TableName(TableName)] + [PrimaryKey("id", AutoIncrement = true)] + [ExplicitColumns] + internal class ContentScheduleDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentSchedule; + + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [NullSetting(NullSetting = NullSettings.Null)] // can be invariant + public int? LanguageId { get; set; } + + [Column("date")] + public DateTime Date { get; set; } + + [Column("action")] + public string Action { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs index fd3df69b8a..be2ad45bcf 100644 --- a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs @@ -1,9 +1,10 @@ -using System; -using NPoco; +using NPoco; +using System.Collections.Generic; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { + [TableName(TableName)] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] @@ -23,14 +24,6 @@ namespace Umbraco.Core.Persistence.Dtos [Column("edited")] public bool Edited { get; set; } - [Column("releaseDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? ReleaseDate { get; set; } - - [Column("expireDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? ExpiresDate { get; set; } - //[Column("publishDate")] //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version //public DateTime? PublishDate { get; set; } @@ -62,5 +55,9 @@ namespace Umbraco.Core.Persistence.Dtos [ResultColumn] [Reference(ReferenceType.OneToOne)] public DocumentVersionDto PublishedVersionDto { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "NodeId")] + public IEnumerable ContentSchedule { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index ec364c7c6a..65b8088176 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; namespace Umbraco.Core.Persistence.Factories { @@ -12,7 +14,7 @@ namespace Umbraco.Core.Persistence.Factories /// /// Builds an IContent item from a dto and content type. /// - public static Content BuildEntity(DocumentDto dto, IContentType contentType) + public static Content BuildEntity(DocumentDto dto, IContentType contentType, ILanguageRepository languageRepository ) { var contentDto = dto.ContentDto; var nodeDto = contentDto.NodeDto; @@ -45,8 +47,18 @@ namespace Umbraco.Core.Persistence.Factories content.Published = dto.Published; content.Edited = dto.Edited; - content.ExpireDate = dto.ExpiresDate; - content.ReleaseDate = dto.ReleaseDate; + + var schedule = new ContentScheduleCollection(); + foreach(var entry in dto.ContentSchedule) + { + schedule.Add(new ContentSchedule(entry.Id, + languageRepository.GetIsoCodeById(entry.LanguageId), + entry.Date, + entry.Action == ContentScheduleChange.Start.ToString() + ? ContentScheduleChange.Start + : ContentScheduleChange.End)); + } + content.ContentSchedule = schedule; // fixme - shall we get published infos or not? //if (dto.Published) @@ -142,7 +154,7 @@ namespace Umbraco.Core.Persistence.Factories content.CreateDate = nodeDto.CreateDate; content.UpdateDate = contentVersionDto.VersionDate; - content.ProviderUserKey = content.Key; // fixme explain + content.ProviderUserKey = content.Key; // The `ProviderUserKey` is a membership provider thing // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -155,9 +167,9 @@ namespace Umbraco.Core.Persistence.Factories } /// - /// Buils a dto from an IContent item. + /// Builds a dto from an IContent item. /// - public static DocumentDto BuildDto(IContent entity, Guid objectType) + public static DocumentDto BuildDto(IContent entity, Guid objectType, ILanguageRepository languageRepository) { var contentDto = BuildContentDto(entity, objectType); @@ -165,13 +177,27 @@ namespace Umbraco.Core.Persistence.Factories { NodeId = entity.Id, Published = entity.Published, - ReleaseDate = entity.ReleaseDate, - ExpiresDate = entity.ExpireDate, - ContentDto = contentDto, DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) }; + var schedule = new List(); + foreach(var schedByCulture in entity.ContentSchedule.GetFullSchedule()) + { + foreach(var cultureSched in schedByCulture.Value) + { + schedule.Add(new ContentScheduleDto + { + Action = cultureSched.Change.ToString(), + Date = cultureSched.Date, + NodeId = entity.Id, + LanguageId = languageRepository.GetIdByIsoCode(schedByCulture.Key), + Id = cultureSched.Id + }); + } + } + dto.ContentSchedule = schedule; + return dto; } diff --git a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs index d2e0e7245c..2cc3a5b140 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs @@ -37,8 +37,6 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.ContentTypeId, dto => dto.ContentTypeId); CacheMap(src => src.UpdateDate, dto => dto.VersionDate); - CacheMap(src => src.ExpireDate, dto => dto.ExpiresDate); - CacheMap(src => src.ReleaseDate, dto => dto.ReleaseDate); CacheMap(src => src.Published, dto => dto.Published); //CacheMap(src => src.Name, dto => dto.Alias); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index f3afe99b28..a427d80bf5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -277,7 +277,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.SanitizeEntityPropertiesForXmlStorage(); // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId, LanguageRepository); // derive path and level from parent var parent = GetParentNodeDto(entity.ParentId); @@ -364,6 +364,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); + //insert the schedule + foreach(var c in dto.ContentSchedule) + { + Database.Insert(c); + } + // persist the variations if (content.ContentType.VariesByCulture()) { @@ -474,7 +480,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId, LanguageRepository); // update the node dto var nodeDto = dto.ContentDto.NodeDto; @@ -582,6 +588,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Update(dto); + //update the schedule, get the existing one so we know what to update + var existingSched = Database.Fetch(Sql() + .Select(x => x.Id) + .Where(x => x.NodeId == content.Id)); + //remove any that no longer exist + var schedToRemove = existingSched.Except(dto.ContentSchedule.Select(x => x.Id)); + foreach(var c in schedToRemove) + Database.DeleteWhere("id = @id", new { id = c }); + //add/update the rest + foreach (var c in dto.ContentSchedule) + { + if (c.Id == 0) + Database.Insert(c); + else + Database.Update(c); + } + // if entity is publishing, update tags, else leave tags there // means that implicitely unpublished, or trashed, entities *still* have tags in db if (content.PublishedState == PublishedState.Publishing) @@ -952,7 +975,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (contentTypes.TryGetValue(contentTypeId, out var contentType) == false) contentTypes[contentTypeId] = contentType = _contentTypeRepository.Get(contentTypeId); - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType, LanguageRepository); if (!slim) { @@ -1024,7 +1047,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IContent MapDtoToContent(DocumentDto dto) { var contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); - var content = ContentBaseFactory.BuildEntity(dto, contentType); + var content = ContentBaseFactory.BuildEntity(dto, contentType, LanguageRepository); // get template if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0) diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs index 73cc752508..16b638e836 100644 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -40,7 +40,10 @@ namespace Umbraco.Core.Publishing { try { - d.ReleaseDate = null; + throw new NotImplementedException("implement scheduled publishing"); + //d.ReleaseDate = null; + //fixme - need to clear this particular schedule for this item + d.PublishCulture(); // fixme variants? var result = _contentService.SaveAndPublish(d, userId: _userService.GetProfileById(d.WriterId).Id); _logger.Debug("Result of publish attempt: {PublishResult}", result.Result); @@ -67,7 +70,10 @@ namespace Umbraco.Core.Publishing { try { - d.ExpireDate = null; + throw new NotImplementedException("implement scheduled publishing"); + //d.ExpireDate = null; + //fixme - need to clear this particular schedule for this item + var result = _contentService.Unpublish(d, userId: _userService.GetProfileById(d.WriterId).Id); if (result.Success) { diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 9bb8e6720e..83f96a122a 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -749,8 +749,11 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published && x.ExpireDate <= DateTime.Now); - return _documentRepository.Get(query); + + //fixme - need to get the DB updated to query for this + //var query = Query().Where(x => x.Published && x.ExpireDate <= DateTime.Now); + //return _documentRepository.Get(query); + throw new NotImplementedException("Implement GetContentForExpiration"); } } @@ -763,8 +766,11 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - return _documentRepository.Get(query); + + //fixme - need to get the DB updated to query for this + //var query = Query().Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); + //return _documentRepository.Get(query); + throw new NotImplementedException("Implement GetContentForRelease"); } } @@ -1241,7 +1247,10 @@ namespace Umbraco.Core.Services.Implement PublishResult result; try { - d.ReleaseDate = null; + //d.ReleaseDate = null; + //fixme - need to clear this particular schedule for this item + throw new NotImplementedException("Implement PerformScheduledPublish"); + d.PublishCulture(); // fixme variants? result = SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) @@ -1258,7 +1267,10 @@ namespace Umbraco.Core.Services.Implement { try { - d.ExpireDate = null; + //d.ExpireDate = null; + //fixme - need to clear this particular schedule for this item + throw new NotImplementedException("Implement PerformScheduledPublish"); + var result = Unpublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -2328,14 +2340,17 @@ namespace Umbraco.Core.Services.Implement if (attempt.Success == false) return attempt; - // if the document has a release date set to before now, - // it should be removed so it doesn't interrupt an unpublish + // 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 - if (content.ReleaseDate.HasValue && content.ReleaseDate.Value <= DateTime.Now) - { - content.ReleaseDate = null; + + var pastReleases = content.ContentSchedule.GetFullSchedule().SelectMany(x => x.Value) + .Where(x => x.Change == ContentScheduleChange.End && x.Date <= DateTime.Now) + .ToList(); + foreach (var p in pastReleases) + content.ContentSchedule.Remove(p); + if(pastReleases.Count > 0) Logger.Info("Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id); - } // change state to unpublishing ((Content) content).PublishedState = PublishedState.Unpublishing; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 03ab648928..15617370e8 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -366,6 +366,7 @@ + @@ -376,6 +377,9 @@ + + + @@ -409,6 +413,7 @@ + diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 807231730b..8f0ba39749 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -228,11 +228,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); //content.ChangePublishedState(PublishedState.Published); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") @@ -287,11 +286,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") { @@ -326,11 +324,11 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.ContentTypeId, content.ContentTypeId); Assert.AreEqual(clone.CreateDate, content.CreateDate); Assert.AreEqual(clone.CreatorId, content.CreatorId); - Assert.AreEqual(clone.ExpireDate, content.ExpireDate); Assert.AreEqual(clone.Key, content.Key); Assert.AreEqual(clone.Level, content.Level); Assert.AreEqual(clone.Path, content.Path); - Assert.AreEqual(clone.ReleaseDate, content.ReleaseDate); + //fixme: Need to maybe override equals or determine another way of comparing these + Assert.AreEqual(clone.ContentSchedule, content.ContentSchedule); Assert.AreEqual(clone.Published, content.Published); Assert.AreEqual(clone.PublishedState, content.PublishedState); Assert.AreEqual(clone.SortOrder, content.SortOrder); @@ -386,11 +384,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); //content.ChangePublishedState(PublishedState.Publishing); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index c5bbe197ef..70e2107ad5 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -260,11 +260,11 @@ namespace Umbraco.Tests.Services // Act var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); - content.ReleaseDate = DateTime.Now.AddHours(2); + content.ContentSchedule.Add(null, DateTime.Now.AddHours(2)); contentService.Save(content, 0); content = contentService.GetById(content.Id); - content.ReleaseDate = null; + content.ContentSchedule.Clear(ContentScheduleChange.End); contentService.Save(content, 0); @@ -1164,7 +1164,7 @@ namespace Umbraco.Tests.Services var root = contentService.GetById(NodeDto.NodeIdSeed + 2); contentService.SaveAndPublish(root); var content = contentService.GetById(NodeDto.NodeIdSeed + 4); - content.ExpireDate = DateTime.Now.AddSeconds(1); + content.ContentSchedule.Add(null, DateTime.Now.AddSeconds(1)); contentService.SaveAndPublish(content); // Act @@ -1504,7 +1504,7 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 4); //This Content expired 5min ago - content.ExpireDate = DateTime.Now.AddMinutes(-5); + content.ContentSchedule.Add(null, DateTime.Now.AddMinutes(-5)); contentService.Save(content); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); @@ -1527,7 +1527,7 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 3); - content.ReleaseDate = DateTime.Now.AddHours(2); + content.ContentSchedule.Add(null, DateTime.Now.AddHours(2)); contentService.Save(content, 0); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); diff --git a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs index b821e5a516..263ca17c28 100644 --- a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs +++ b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Services //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1062 Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - subpage.ReleaseDate = DateTime.Now.AddMinutes(-5); + subpage.ContentSchedule.Add(null, DateTime.Now.AddMinutes(-5)); ServiceContext.ContentService.Save(subpage, 0); //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1063 diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 53e2593fef..7f16d66480 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1425,8 +1425,8 @@ namespace Umbraco.Web.Editors //TODO: We need to support 'send to publish' - contentSave.PersistedContent.ExpireDate = contentSave.ExpireDate; - contentSave.PersistedContent.ReleaseDate = contentSave.ReleaseDate; + //fixme - deal with variants too + contentSave.PersistedContent.ContentSchedule.Add(contentSave.ExpireDate, contentSave.ReleaseDate); //only set the template if it didn't change var templateChanged = (contentSave.PersistedContent.Template == null && contentSave.TemplateAlias.IsNullOrWhiteSpace() == false)