diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs
index d73c948094..94f6d0beb7 100644
--- a/src/Umbraco.Core/Models/Content.cs
+++ b/src/Umbraco.Core/Models/Content.cs
@@ -92,10 +92,36 @@ namespace Umbraco.Core.Models
}
///
+ [DoNotClone]
public ContentScheduleCollection ContentSchedule
{
- get => _schedule ?? (_schedule = new ContentScheduleCollection());
- set => SetPropertyValueAndDetectChanges(value, ref _schedule, Ps.Value.ContentScheduleSelector);
+ get
+ {
+ if (_schedule == null)
+ {
+ _schedule = new ContentScheduleCollection();
+ _schedule.CollectionChanged += ScheduleCollectionChanged;
+ }
+ return _schedule ?? (_schedule = new ContentScheduleCollection());
+ }
+ set
+ {
+ if(_schedule != null)
+ _schedule.CollectionChanged -= ScheduleCollectionChanged;
+ SetPropertyValueAndDetectChanges(value, ref _schedule, Ps.Value.ContentScheduleSelector);
+ 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(Ps.Value.ContentScheduleSelector);
}
///
@@ -125,13 +151,13 @@ namespace Umbraco.Core.Models
return ContentStatus.Trashed;
//fixme - deal with variants
- var expires = ContentSchedule.GetSchedule(ContentScheduleChange.End).FirstOrDefault();
- if (expires != null && expires.Date > DateTime.MinValue && DateTime.Now > expires.Date)
+ var expires = ContentSchedule.GetSchedule(ContentScheduleChange.End);
+ if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date))
return ContentStatus.Expired;
//fixme - deal with variants
- var release = ContentSchedule.GetSchedule(ContentScheduleChange.Start).FirstOrDefault();
- if (release != null && release.Date > DateTime.MinValue && release.Date > DateTime.Now)
+ var release = ContentSchedule.GetSchedule(ContentScheduleChange.Start);
+ if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now))
return ContentStatus.AwaitingRelease;
if(Published)
@@ -508,6 +534,14 @@ namespace Umbraco.Core.Models
clone._publishInfos.CollectionChanged += clone.PublishNamesCollectionChanged; //re-assign correct event handler
}
+ //if properties exist then deal with event bindings
+ if (clone._schedule != null)
+ {
+ clone._schedule.CollectionChanged -= ScheduleCollectionChanged; //clear this event handler if any
+ clone._schedule = (ContentScheduleCollection)_schedule.DeepClone(); //manually deep clone
+ clone._schedule.CollectionChanged += clone.ScheduleCollectionChanged; //re-assign correct event handler
+ }
+
//re-enable tracking
clone.EnableChangeTracking();
diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs
index f137b85ee7..d2905ed54d 100644
--- a/src/Umbraco.Core/Models/ContentSchedule.cs
+++ b/src/Umbraco.Core/Models/ContentSchedule.cs
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Models
///
[Serializable]
[DataContract(IsReference = true)]
- public class ContentSchedule
+ public class ContentSchedule : IDeepCloneable
{
public ContentSchedule(int id, string culture, DateTime date, ContentScheduleChange change)
{
@@ -45,5 +45,10 @@ namespace Umbraco.Core.Models
///
[DataMember]
public ContentScheduleChange Change { get; }
+
+ public object DeepClone()
+ {
+ return new ContentSchedule(Id, Culture, Date, Change);
+ }
}
}
diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs
index 73607271a3..52dcad9e45 100644
--- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs
+++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs
@@ -2,14 +2,22 @@
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Collections.Specialized;
namespace Umbraco.Core.Models
{
- public class ContentScheduleCollection
+ public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable
{
//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>();
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ private void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
+ {
+ CollectionChanged?.Invoke(this, args);
+ }
+
///
/// Add an existing schedule
///
@@ -24,6 +32,8 @@ namespace Umbraco.Core.Models
//TODO: Below will throw if there are duplicate dates added, validate/return bool?
changes.Add(schedule.Date, schedule);
+
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule));
}
///
@@ -62,9 +72,20 @@ namespace Umbraco.Core.Models
// 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));
+ {
+ var entry = new ContentSchedule(0, culture, releaseDate.Value, ContentScheduleChange.Start);
+ changes.Add(releaseDate.Value, entry);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry));
+ }
+
if (expireDate.HasValue)
- changes.Add(expireDate.Value, new ContentSchedule(0, culture, expireDate.Value, ContentScheduleChange.End));
+ {
+ var entry = new ContentSchedule(0, culture, expireDate.Value, ContentScheduleChange.End);
+ changes.Add(expireDate.Value, entry);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry));
+ }
+
+
}
@@ -76,7 +97,14 @@ namespace Umbraco.Core.Models
{
if (_schedule.TryGetValue(change.Culture, out var s))
{
- s.Remove(change.Date);
+ var removed = s.Remove(change.Date);
+ if (removed)
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change));
+ if (s.Count == 0)
+ _schedule.Remove(change.Culture);
+ }
+
}
}
@@ -99,7 +127,16 @@ namespace Umbraco.Core.Models
if (_schedule.TryGetValue(culture, out var s))
{
foreach (var ofChange in s.Where(x => x.Value.Change == changeType).ToList())
- s.Remove(ofChange.Value.Date);
+ {
+ var removed = s.Remove(ofChange.Value.Date);
+ if (removed)
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, ofChange.Value));
+ if (s.Count == 0)
+ _schedule.Remove(culture);
+ }
+ }
+
}
}
@@ -132,5 +169,18 @@ namespace Umbraco.Core.Models
{
return _schedule.ToDictionary(x => x.Key, x => (IEnumerable)x.Value.Values);
}
+
+ public object DeepClone()
+ {
+ var clone = new ContentScheduleCollection();
+ foreach(var cultureSched in _schedule)
+ {
+ var list = new SortedList();
+ foreach (var schedEntry in cultureSched.Value)
+ list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone());
+ clone._schedule[cultureSched.Key] = list;
+ }
+ return clone;
+ }
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs
index be2ad45bcf..30e8f295fb 100644
--- a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs
@@ -55,9 +55,6 @@ 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 8ad4f96396..fc81f3e6a4 100644
--- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
@@ -15,7 +15,7 @@ namespace Umbraco.Core.Persistence.Factories
///
/// Builds an IContent item from a dto and content type.
///
- public static Content BuildEntity(DocumentDto dto, IContentType contentType, ILanguageRepository languageRepository )
+ public static Content BuildEntity(DocumentDto dto, IContentType contentType)
{
var contentDto = dto.ContentDto;
var nodeDto = contentDto.NodeDto;
@@ -49,18 +49,6 @@ namespace Umbraco.Core.Persistence.Factories
content.Published = dto.Published;
content.Edited = dto.Edited;
- var schedule = new ContentScheduleCollection();
- foreach(var entry in dto.ContentSchedule ?? Enumerable.Empty())
- {
- 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)
if (publishedVersionDto != null)
@@ -170,7 +158,7 @@ namespace Umbraco.Core.Persistence.Factories
///
/// Builds a dto from an IContent item.
///
- public static DocumentDto BuildDto(IContent entity, Guid objectType, ILanguageRepository languageRepository)
+ public static DocumentDto BuildDto(IContent entity, Guid objectType)
{
var contentDto = BuildContentDto(entity, objectType);
@@ -182,10 +170,15 @@ namespace Umbraco.Core.Persistence.Factories
DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto)
};
+ return dto;
+ }
+
+ public static IEnumerable BuildScheduleDto(IContent entity, ILanguageRepository languageRepository)
+ {
var schedule = new List();
- foreach(var schedByCulture in entity.ContentSchedule.GetFullSchedule())
+ foreach (var schedByCulture in entity.ContentSchedule.GetFullSchedule())
{
- foreach(var cultureSched in schedByCulture.Value)
+ foreach (var cultureSched in schedByCulture.Value)
{
schedule.Add(new ContentScheduleDto
{
@@ -197,9 +190,7 @@ namespace Umbraco.Core.Persistence.Factories
});
}
}
- dto.ContentSchedule = schedule;
-
- return dto;
+ return schedule;
}
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index 2bb930dd79..a3dee9317f 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -121,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 =>
r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto, "pcv")))
- // select the variant name, coalesce to the invariant name, as "variantName"
+ // select the variant name, coalesce to the invariant name, as "variantName"
.AndSelect(VariantNameSqlExpression + " AS variantName");
break;
}
@@ -143,11 +143,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv")
.On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv")
+ //fixme - should we be joining this when the query type is not single/many?
// left join on optional culture variation
//the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code
.LeftJoin(nested =>
- nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv")
- .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv");
+ nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv")
+ .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv");
sql
.Where(x => x.NodeObjectType == NodeObjectTypeId);
@@ -261,7 +262,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// however, it's not just so we have access to AddingEntity
// there are tons of things at the end of the methods, that can only work with a true Content
// and basically, the repository requires a Content, not an IContent
- var content = (Content) entity;
+ var content = (Content)entity;
content.AddingEntity();
var publishing = content.PublishedState == PublishedState.Publishing;
@@ -278,7 +279,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
entity.SanitizeEntityPropertiesForXmlStorage();
// create the dto
- var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId, LanguageRepository);
+ var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId);
// derive path and level from parent
var parent = GetParentNodeDto(entity.ParentId);
@@ -366,7 +367,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.Insert(dto);
//insert the schedule
- foreach(var c in dto.ContentSchedule)
+ var scheduleDto = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository);
+ foreach (var c in scheduleDto)
{
c.NodeId = nodeDto.NodeId;
Database.Insert(c);
@@ -442,10 +444,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// however, it's not just so we have access to AddingEntity
// there are tons of things at the end of the methods, that can only work with a true Content
// and basically, the repository requires a Content, not an IContent
- var content = (Content) entity;
+ var content = (Content)entity;
// check if we need to make any database changes at all
- if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished) && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty())
+ if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished)
+ && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty())
return; // no change to save, do nothing, don't even update dates
// whatever we do, we must check that we are saving the current version
@@ -482,7 +485,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
// create the dto
- var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId, LanguageRepository);
+ var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId);
// update the node dto
var nodeDto = dto.ContentDto.NodeDto;
@@ -590,22 +593,26 @@ 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)
- .From()
- .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 (content.IsPropertyDirty("ContentSchedule"))
{
- if (c.Id == 0)
- Database.Insert(c);
- else
- Database.Update(c);
+ //update the schedule, get the existing one so we know what to update
+ var dtoSchedule = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository);
+ var existingSched = Database.Fetch(Sql()
+ .Select(x => x.Id)
+ .From()
+ .Where(x => x.NodeId == content.Id));
+ //remove any that no longer exist
+ var schedToRemove = existingSched.Except(dtoSchedule.Select(x => x.Id));
+ foreach (var c in schedToRemove)
+ Database.DeleteWhere("id = @id", new { id = c });
+ //add/update the rest
+ foreach (var c in dtoSchedule)
+ {
+ if (c.Id == 0)
+ Database.Insert(c);
+ else
+ Database.Update(c);
+ }
}
// if entity is publishing, update tags, else leave tags there
@@ -760,7 +767,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
foreach (var filterClause in filter.GetWhereClauses())
{
var clauseSql = filterClause.Item1;
- var clauseArgs = filterClause.Item2;
+ var clauseArgs = filterClause.Item2;
// replace the name field
// we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here
@@ -1011,7 +1018,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id)
{
- content[i] = (Content) cached;
+ content[i] = (Content)cached;
continue;
}
}
@@ -1024,7 +1031,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, LanguageRepository);
+ var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType);
if (!slim)
{
@@ -1059,6 +1066,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// load all properties for all documents from database in 1 query - indexed by version id
var properties = GetPropertyCollections(temps);
+ var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray());
// assign templates and properties
foreach (var temp in temps)
@@ -1069,10 +1077,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (temp.Template2Id.HasValue && templates.TryGetValue(temp.Template2Id.Value, out template))
temp.Content.PublishTemplate = template;
- if (properties.ContainsKey(temp.VersionId))
- temp.Content.Properties = properties[temp.VersionId];
- else
- throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'.");
+ if (properties.ContainsKey(temp.VersionId))
+ temp.Content.Properties = properties[temp.VersionId];
+ else
+ throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'.");
+
+ //load in the schedule
+ if (schedule.TryGetValue(temp.Content.Id, out var s))
+ temp.Content.ContentSchedule = s;
// reset dirty initial properties (U4-1946)
temp.Content.ResetDirtyProperties(false);
@@ -1096,35 +1108,72 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private IContent MapDtoToContent(DocumentDto dto)
{
var contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId);
- var content = ContentBaseFactory.BuildEntity(dto, contentType, LanguageRepository);
+ var content = ContentBaseFactory.BuildEntity(dto, contentType);
- // get template
- if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0)
- content.Template = _templateRepository.Get(dto.DocumentVersionDto.TemplateId.Value);
-
- // get properties - indexed by version id
- var versionId = dto.DocumentVersionDto.Id;
-
- // fixme - shall we get published properties or not?
- //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0;
- var publishedVersionId = dto.PublishedVersionDto != null ? dto.PublishedVersionDto.Id : 0;
-
- var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType);
- var ltemp = new List> { temp };
- var properties = GetPropertyCollections(ltemp);
- content.Properties = properties[dto.DocumentVersionDto.Id];
-
- // set variations, if varying
- if (contentType.VariesByCulture())
+ try
{
- var contentVariations = GetContentVariations(ltemp);
- var documentVariations = GetDocumentVariations(ltemp);
- SetVariations(content, contentVariations, documentVariations);
- }
+ content.DisableChangeTracking();
- // reset dirty initial properties (U4-1946)
- content.ResetDirtyProperties(false);
- return content;
+ // get template
+ if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0)
+ content.Template = _templateRepository.Get(dto.DocumentVersionDto.TemplateId.Value);
+
+ // get properties - indexed by version id
+ var versionId = dto.DocumentVersionDto.Id;
+
+ // fixme - shall we get published properties or not?
+ //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0;
+ var publishedVersionId = dto.PublishedVersionDto != null ? dto.PublishedVersionDto.Id : 0;
+
+ var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType);
+ var ltemp = new List> { temp };
+ var properties = GetPropertyCollections(ltemp);
+ content.Properties = properties[dto.DocumentVersionDto.Id];
+
+ // set variations, if varying
+ if (contentType.VariesByCulture())
+ {
+ var contentVariations = GetContentVariations(ltemp);
+ var documentVariations = GetDocumentVariations(ltemp);
+ 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;
+ }
+ finally
+ {
+ content.EnableChangeTracking();
+ }
+ }
+
+ private IDictionary GetContentSchedule(params int[] contentIds)
+ {
+ var result = new Dictionary();
+ foreach(var ids in contentIds.InGroupsOf(2000))
+ {
+ var sql = Sql().Select().From().Where(x => ids.Contains(x.NodeId));
+ var scheduleDto = Database.Fetch(sql);
+
+ foreach (var dto in scheduleDto)
+ {
+ var schedule = new ContentScheduleCollection();
+ schedule.Add(new ContentSchedule(dto.Id,
+ LanguageRepository.GetIsoCodeById(dto.LanguageId) ?? string.Empty,
+ dto.Date,
+ dto.Action == ContentScheduleChange.Start.ToString()
+ ? ContentScheduleChange.Start
+ : ContentScheduleChange.End));
+ result[dto.NodeId] = schedule;
+ }
+ }
+ return result;
}
private void SetVariations(Content content, IDictionary> contentVariations, IDictionary> documentVariations)
diff --git a/src/Umbraco.Tests/Models/ContentScheduleTests.cs b/src/Umbraco.Tests/Models/ContentScheduleTests.cs
new file mode 100644
index 0000000000..1e28b2e9b8
--- /dev/null
+++ b/src/Umbraco.Tests/Models/ContentScheduleTests.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Linq;
+using NUnit.Framework;
+using Umbraco.Core.Models;
+
+namespace Umbraco.Tests.Models
+{
+ [TestFixture]
+ public class ContentScheduleTests
+ {
+ [Test]
+ public void Release_Date_Less_Than_Expire_Date()
+ {
+ var now = DateTime.Now;
+ var schedule = new ContentScheduleCollection();
+ Assert.Throws(() => schedule.Add(now, now));
+ }
+
+ [Test]
+ public void Cannot_Add_Duplicate_Dates_Invariant()
+ {
+ var now = DateTime.Now;
+ var schedule = new ContentScheduleCollection();
+ schedule.Add(now, null);
+ Assert.Throws(() => schedule.Add(null, now));
+ }
+
+ [Test]
+ public void Cannot_Add_Duplicate_Dates_Variant()
+ {
+ var now = DateTime.Now;
+ var schedule = new ContentScheduleCollection();
+ schedule.Add(now, null);
+ schedule.Add("en-US", now, null);
+ Assert.Throws(() => schedule.Add("en-US", null, now));
+ Assert.Throws(() => schedule.Add(null, now));
+ }
+
+ [Test]
+ public void Can_Remove_Invariant()
+ {
+ var now = DateTime.Now;
+ var schedule = new ContentScheduleCollection();
+ schedule.Add(now, null);
+ var invariantSched = schedule.GetSchedule(string.Empty);
+ schedule.Remove(invariantSched.First());
+ Assert.AreEqual(0, schedule.GetFullSchedule().Count());
+ }
+
+ [Test]
+ public void Can_Remove_Variant()
+ {
+ var now = DateTime.Now;
+ var schedule = new ContentScheduleCollection();
+ schedule.Add(now, null);
+ schedule.Add("en-US", now, null);
+ var invariantSched = schedule.GetSchedule(string.Empty);
+ schedule.Remove(invariantSched.First());
+ Assert.AreEqual(0, schedule.GetSchedule(string.Empty).Count());
+ Assert.AreEqual(1, schedule.GetFullSchedule().Count());
+ var variantSched = schedule.GetSchedule("en-US");
+ schedule.Remove(variantSched.First());
+ Assert.AreEqual(0, schedule.GetSchedule("en-US").Count());
+ Assert.AreEqual(0, schedule.GetFullSchedule().Count());
+ }
+
+ [Test]
+ public void Can_Clear_Start_Invariant()
+ {
+ var now = DateTime.Now;
+ var schedule = new ContentScheduleCollection();
+ schedule.Add(now, now.AddDays(1));
+
+ schedule.Clear(ContentScheduleChange.Start);
+
+ Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleChange.Start).Count());
+ Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleChange.End).Count());
+ Assert.AreEqual(1, schedule.GetFullSchedule().Count());
+ }
+
+ [Test]
+ public void Can_Clear_End_Variant()
+ {
+ var now = DateTime.Now;
+ var schedule = new ContentScheduleCollection();
+ schedule.Add(now, now.AddDays(1));
+ schedule.Add("en-US", now, now.AddDays(1));
+
+ schedule.Clear(ContentScheduleChange.End);
+
+ Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleChange.End).Count());
+ Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleChange.Start).Count());
+ Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleChange.End).Count());
+ Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleChange.Start).Count());
+ Assert.AreEqual(2, schedule.GetFullSchedule().Count());
+
+ schedule.Clear("en-US", ContentScheduleChange.End);
+
+ Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleChange.End).Count());
+ Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleChange.Start).Count());
+ Assert.AreEqual(0, schedule.GetSchedule("en-US", ContentScheduleChange.End).Count());
+ Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleChange.Start).Count());
+ Assert.AreEqual(2, schedule.GetFullSchedule().Count());
+ }
+
+ }
+}
diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs
index 8f0ba39749..211de9a645 100644
--- a/src/Umbraco.Tests/Models/ContentTests.cs
+++ b/src/Umbraco.Tests/Models/ContentTests.cs
@@ -22,6 +22,7 @@ using Umbraco.Tests.Testing;
namespace Umbraco.Tests.Models
{
+
[TestFixture]
public class ContentTests : UmbracoTestBase
{
diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs
index 2394302aad..35b5ed0c0d 100644
--- a/src/Umbraco.Tests/Services/ContentServiceTests.cs
+++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs
@@ -260,11 +260,17 @@ namespace Umbraco.Tests.Services
contentService.Save(content, Constants.Security.SuperUserId);
content = contentService.GetById(content.Id);
+ var sched = content.ContentSchedule.GetFullSchedule();
+ Assert.AreEqual(1, sched.Count());
+ Assert.AreEqual(1, sched[string.Empty].Count());
content.ContentSchedule.Clear(ContentScheduleChange.End);
contentService.Save(content, Constants.Security.SuperUserId);
// Assert
+ content = contentService.GetById(content.Id);
+ sched = content.ContentSchedule.GetFullSchedule();
+ Assert.AreEqual(0, sched.Count());
Assert.IsTrue(contentService.SaveAndPublish(content).Success);
}
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index 156bc06a14..8cff47c41b 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -122,6 +122,7 @@
+