diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index ce40bd9baa..5681a31948 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2018")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2019")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs index 241b84d8d2..2bd4c0d434 100644 --- a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs @@ -78,33 +78,6 @@ namespace Umbraco.Core.Composing return This; } - /// - /// Appends a type after another type. - /// - /// The other type. - /// The type to append. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder AppendAfter() - where TAfter : TItem - where T : TItem - { - Configure(types => - { - var typeAfter = typeof (TAfter); - var type = typeof(T); - if (typeAfter == type) throw new InvalidOperationException(); - - var index = types.IndexOf(typeAfter); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeAfter); // in case removing type changed index - types.Insert(index + 1, type); - }); - return This; - } - /// /// Inserts a type into the collection. /// @@ -206,6 +179,69 @@ namespace Umbraco.Core.Composing return This; } + /// + /// Inserts a type after another type. + /// + /// The other type. + /// The type to append. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertAfter() + where TAfter : TItem + where T : TItem + { + Configure(types => + { + var typeAfter = typeof(TAfter); + var type = typeof(T); + if (typeAfter == type) throw new InvalidOperationException(); + + var index = types.IndexOf(typeAfter); + if (index < 0) throw new InvalidOperationException(); + + if (types.Contains(type)) types.Remove(type); + index = types.IndexOf(typeAfter); // in case removing type changed index + index += 1; // insert here + + if (index == types.Count) + types.Add(type); + else + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type after another type. + /// + /// The other type. + /// The type to insert. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertAfter(Type typeAfter, Type type) + { + Configure(types => + { + EnsureType(typeAfter, "find"); + EnsureType(type, "register"); + + if (typeAfter == type) throw new InvalidOperationException(); + + var index = types.IndexOf(typeAfter); + if (index < 0) throw new InvalidOperationException(); + + if (types.Contains(type)) types.Remove(type); + index = types.IndexOf(typeAfter); // in case removing type changed index + index += 1; // insert here + + if (index == types.Count) + types.Add(type); + else + types.Insert(index, type); + }); + return This; + } + /// /// Removes a type from the collection. /// diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index bf2e7f6aa8..d82f181e5d 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -52,36 +52,8 @@ namespace Umbraco.Core return ContentStatus.Unpublished; } - /// - /// Gets the cultures that have been flagged for unpublishing. - /// - /// Gets cultures for which content.UnpublishCulture() has been invoked. - internal static IReadOnlyList GetCulturesUnpublishing(this IContent content) - { - if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos")) - return Array.Empty(); - - var culturesChanging = content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key); - return culturesChanging - .Where(x => !content.IsCulturePublished(x) && // is not published anymore - content.WasCulturePublished(x)) // but was published before - .ToList(); - } - - /// - /// Returns true if this entity was just published as part of a recent save operation (i.e. it wasn't previously published) - /// - /// - /// - /// - /// This is helpful for determining if the published event will execute during the saved event for a content item. - /// - internal static bool JustPublished(this IContent entity) - { - var dirty = (IRememberBeingDirty)entity; - return dirty.WasPropertyDirty("Published") && entity.Published; - } - + + #endregion /// diff --git a/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs new file mode 100644 index 0000000000..1a651ef348 --- /dev/null +++ b/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Permissions; + +namespace Umbraco.Core.Events +{ + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, IEquatable> + { + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) + : base(eventObject) + { } + + public bool Equals(CancellableEnumerableObjectEventArgs other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + return EventObject.SequenceEqual(other.EventObject); + } + + public override bool Equals(object obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableEnumerableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + return HashCodeHelper.GetHashCode(EventObject); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs index 0f3091c46a..19f576478f 100644 --- a/src/Umbraco.Core/Events/CancellableEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Events public class CancellableEventArgs : EventArgs, IEquatable { private bool _cancel; - private Dictionary _eventState; + private IDictionary _eventState; private static readonly ReadOnlyDictionary EmptyAdditionalData = new ReadOnlyDictionary(new Dictionary()); @@ -89,7 +89,7 @@ namespace Umbraco.Core.Events /// /// Returns the EventMessages object which is used to add messages to the message collection for this event /// - public EventMessages Messages { get; private set; } + public EventMessages Messages { get; } /// /// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event subscribers @@ -98,7 +98,7 @@ namespace Umbraco.Core.Events /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility /// so we cannot change the strongly typed nature for some events. /// - public ReadOnlyDictionary AdditionalData { get; private set; } + public ReadOnlyDictionary AdditionalData { get; internal set; } /// /// This can be used by event subscribers to store state in the event args so they easily deal with custom state data between a starting ("ing") @@ -106,7 +106,8 @@ namespace Umbraco.Core.Events /// public IDictionary EventState { - get { return _eventState ?? (_eventState = new Dictionary()); } + get => _eventState ?? (_eventState = new Dictionary()); + internal set => _eventState = value; } public bool Equals(CancellableEventArgs other) diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index a64a399249..27ffb1b75d 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Security.Permissions; using Umbraco.Core.Models; @@ -128,49 +127,4 @@ namespace Umbraco.Core.Events return !Equals(left, right); } } - - [HostProtection(SecurityAction.LinkDemand, SharedState = true)] - public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, IEquatable> - { - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) - { } - - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) - { } - - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) - { } - - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) - : base(eventObject, canCancel) - { } - - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) - : base(eventObject) - { } - - public bool Equals(CancellableEnumerableObjectEventArgs other) - { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - - return EventObject.SequenceEqual(other.EventObject); - } - - public override bool Equals(object obj) - { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CancellableEnumerableObjectEventArgs)obj); - } - - public override int GetHashCode() - { - return HashCodeHelper.GetHashCode(EventObject); - } - } } diff --git a/src/Umbraco.Core/Events/ContentPublishedEventArgs.cs b/src/Umbraco.Core/Events/ContentPublishedEventArgs.cs new file mode 100644 index 0000000000..589e447ba7 --- /dev/null +++ b/src/Umbraco.Core/Events/ContentPublishedEventArgs.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Events +{ + public class ContentPublishedEventArgs : PublishEventArgs + { + public ContentPublishedEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } + + /// + /// Determines whether a culture has been published, during a Published event. + /// + public bool HasPublishedCulture(IContent content, string culture) + => content.WasPropertyDirty("_changedCulture_" + culture); + + /// + /// Determines whether a culture has been unpublished, during a Published event. + /// + public bool HasUnpublishedCulture(IContent content, string culture) + => content.WasPropertyDirty("_unpublishedCulture_" + culture); + + + + } +} diff --git a/src/Umbraco.Core/Events/ContentPublishingEventArgs.cs b/src/Umbraco.Core/Events/ContentPublishingEventArgs.cs new file mode 100644 index 0000000000..35cbf63441 --- /dev/null +++ b/src/Umbraco.Core/Events/ContentPublishingEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Events +{ + public class ContentPublishingEventArgs : PublishEventArgs + { + /// + /// Creates a new + /// + /// + /// + public ContentPublishingEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + /// + /// Determines whether a culture is being published, during a Publishing event. + /// + public bool IsPublishingCulture(IContent content, string culture) + => content.PublishCultureInfos.TryGetValue(culture, out var cultureInfo) && cultureInfo.IsDirty(); + + /// + /// Determines whether a culture is being unpublished, during a Publishing event. + /// + public bool IsUnpublishingCulture(IContent content, string culture) + => content.IsPropertyDirty("_unpublishedCulture_" + culture); //bit of a hack since we know that the content implementation tracks changes this way + + } +} diff --git a/src/Umbraco.Core/Events/ContentSavedEventArgs.cs b/src/Umbraco.Core/Events/ContentSavedEventArgs.cs new file mode 100644 index 0000000000..4d4085b064 --- /dev/null +++ b/src/Umbraco.Core/Events/ContentSavedEventArgs.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Events +{ + public class ContentSavedEventArgs : SaveEventArgs + { + #region Constructors + + /// + /// Creates a new + /// + /// + /// + /// + public ContentSavedEventArgs(IEnumerable eventObject, EventMessages messages, IDictionary additionalData) + : base(eventObject, false, messages, additionalData) + { + } + + #endregion + + /// + /// Determines whether a culture has been saved, during a Saved event. + /// + public bool HasSavedCulture(IContent content, string culture) + => content.WasPropertyDirty("_updatedCulture_" + culture); + } +} diff --git a/src/Umbraco.Core/Events/ContentSavingEventArgs.cs b/src/Umbraco.Core/Events/ContentSavingEventArgs.cs new file mode 100644 index 0000000000..aa62f64349 --- /dev/null +++ b/src/Umbraco.Core/Events/ContentSavingEventArgs.cs @@ -0,0 +1,66 @@ +using System.Linq; +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Events +{ + public class ContentSavingEventArgs : SaveEventArgs + { + #region Factory Methods + /// + /// Converts to while preserving all args state + /// + /// + public ContentSavedEventArgs ToContentSavedEventArgs() + { + return new ContentSavedEventArgs(EventObject, Messages, AdditionalData) + { + EventState = EventState + }; + } + + /// + /// Converts to while preserving all args state + /// + /// + public ContentPublishedEventArgs ToContentPublishedEventArgs() + { + return new ContentPublishedEventArgs(EventObject, false, Messages) + { + EventState = EventState, + AdditionalData = AdditionalData + }; + } + + /// + /// Converts to while preserving all args state + /// + /// + public ContentPublishingEventArgs ToContentPublishingEventArgs() + { + return new ContentPublishingEventArgs(EventObject, Messages) + { + EventState = EventState, + AdditionalData = AdditionalData + }; + } + #endregion + + #region Constructors + + public ContentSavingEventArgs(IEnumerable eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) + { + } + + public ContentSavingEventArgs(IContent eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) + { + } + + #endregion + + /// + /// Determines whether a culture is being saved, during a Saving event. + /// + public bool IsSavingCulture(IContent content, string culture) => content.CultureInfos.TryGetValue(culture, out var cultureInfo) && cultureInfo.IsDirty(); + } +} diff --git a/src/Umbraco.Core/Events/EventNameExtractor.cs b/src/Umbraco.Core/Events/EventNameExtractor.cs index 5cb9ca64ef..627426f2ee 100644 --- a/src/Umbraco.Core/Events/EventNameExtractor.cs +++ b/src/Umbraco.Core/Events/EventNameExtractor.cs @@ -35,6 +35,24 @@ namespace Umbraco.Core.Events /// null if not found or an ambiguous match /// public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) + { + var events = FindEvents(senderType, argsType, exclude); + + switch (events.Length) + { + case 0: + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); + + case 1: + return Attempt.Succeed(new EventNameExtractorResult(events[0])); + + default: + //there's more than one left so it's ambiguous! + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); + } + } + + internal static string[] FindEvents(Type senderType, Type argsType, Func exclude) { var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => { @@ -78,16 +96,7 @@ namespace Umbraco.Core.Events }).Select(x => x.EventInfo.Name).ToArray(); }); - var filtered = found.Where(x => exclude(x) == false).ToArray(); - - if (filtered.Length == 0) - return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); - - if (filtered.Length == 1) - return Attempt.Succeed(new EventNameExtractorResult(filtered[0])); - - //there's more than one left so it's ambiguous! - return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); + return found.Where(x => exclude(x) == false).ToArray(); } /// diff --git a/src/Umbraco.Core/Events/PublishEventArgs.cs b/src/Umbraco.Core/Events/PublishEventArgs.cs index 599bae1639..4be655873e 100644 --- a/src/Umbraco.Core/Events/PublishEventArgs.cs +++ b/src/Umbraco.Core/Events/PublishEventArgs.cs @@ -10,12 +10,10 @@ namespace Umbraco.Core.Events /// /// /// - /// /// - public PublishEventArgs(IEnumerable eventObject, bool canCancel, bool isAllPublished, EventMessages eventMessages) + public PublishEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) : base(eventObject, canCancel, eventMessages) { - IsAllRepublished = isAllPublished; } /// @@ -43,12 +41,10 @@ namespace Umbraco.Core.Events /// /// /// - /// /// - public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished, EventMessages eventMessages) + public PublishEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) : base(new List { eventObject }, canCancel, eventMessages) { - IsAllRepublished = isAllPublished; } /// @@ -60,7 +56,6 @@ namespace Umbraco.Core.Events public PublishEventArgs(IEnumerable eventObject, bool canCancel, bool isAllPublished) : base(eventObject, canCancel) { - IsAllRepublished = isAllPublished; } /// @@ -90,24 +85,18 @@ namespace Umbraco.Core.Events public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished) : base(new List { eventObject }, canCancel) { - IsAllRepublished = isAllPublished; } /// /// Returns all entities that were published during the operation /// - public IEnumerable PublishedEntities - { - get { return EventObject; } - } - - public bool IsAllRepublished { get; private set; } + public IEnumerable PublishedEntities => EventObject; public bool Equals(PublishEventArgs other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && IsAllRepublished == other.IsAllRepublished; + return base.Equals(other); } public override bool Equals(object obj) @@ -122,7 +111,7 @@ namespace Umbraco.Core.Events { unchecked { - return (base.GetHashCode() * 397) ^ IsAllRepublished.GetHashCode(); + return (base.GetHashCode() * 397); } } diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index 8c3fe82e2f..0b061c2227 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; namespace Umbraco.Core.Events { @@ -113,9 +112,6 @@ namespace Umbraco.Core.Events /// /// Returns all entities that were saved during the operation /// - public IEnumerable SavedEntities - { - get { return EventObject; } - } + public IEnumerable SavedEntities => EventObject; } } diff --git a/src/Umbraco.Core/Exceptions/BootFailedException.cs b/src/Umbraco.Core/Exceptions/BootFailedException.cs index 10a648fd76..c3262d26c6 100644 --- a/src/Umbraco.Core/Exceptions/BootFailedException.cs +++ b/src/Umbraco.Core/Exceptions/BootFailedException.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Exceptions /// /// Defines the default boot failed exception message. /// - public const string DefaultMessage = "Boot failed: Umbraco cannot run. Sad. See Umbraco's log file for more details."; + public const string DefaultMessage = "Boot failed: Umbraco cannot run. See Umbraco's log file for more details."; /// /// Initializes a new instance of the class with a specified error message. diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 23c835a327..84dd393b0d 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using NPoco; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 @@ -59,5 +61,58 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Database.Update(dto); } } + + // we *need* to use this private DTO here, which does *not* have extra properties, which would kill the migration + + [TableName(TableName)] + [PrimaryKey("pk")] + [ExplicitColumns] + private class ContentTypeDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 535)] + public int PrimaryKey { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Alias { get; set; } + + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string Icon { get; set; } + + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string Thumbnail { get; set; } + + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string Description { get; set; } + + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } + + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + public NodeDto NodeDto { get; set; } + } + } } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index d33e78d422..41d2ea63bd 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -18,9 +18,15 @@ namespace Umbraco.Core.Models private ContentScheduleCollection _schedule; private bool _published; private PublishedState _publishedState; - private ContentCultureInfosCollection _publishInfos; - private ContentCultureInfosCollection _publishInfosOrig; private HashSet _editedCultures; + private ContentCultureInfosCollection _publishInfos; + + #region Used for change tracking + + private (HashSet addedCultures, HashSet removedCultures, HashSet updatedCultures) _currentPublishCultureChanges; + private (HashSet addedCultures, HashSet removedCultures, HashSet updatedCultures) _previousPublishCultureChanges; + + #endregion /// /// Constructor for creating a Content object @@ -91,7 +97,7 @@ namespace Umbraco.Core.Models } set { - if(_schedule != null) + if (_schedule != null) _schedule.CollectionChanged -= ScheduleCollectionChanged; SetPropertyValueAndDetectChanges(value, ref _schedule, nameof(ContentSchedule)); if (_schedule != null) @@ -127,15 +133,16 @@ namespace Umbraco.Core.Models /// /// Gets or sets a value indicating whether this content item is published or not. /// + /// + /// the setter is should only be invoked from + /// - the ContentFactory when creating a content entity from a dto + /// - the ContentRepository when updating a content entity + /// [DataMember] public bool Published { get => _published; - - // the setter is internal and should only be invoked from - // - the ContentFactory when creating a content entity from a dto - // - the ContentRepository when updating a content entity - internal set + set { SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published)); _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; @@ -161,27 +168,31 @@ namespace Umbraco.Core.Models } [IgnoreDataMember] - public bool Edited { get; internal set; } + public bool Edited { get; set; } /// [IgnoreDataMember] - public DateTime? PublishDate { get; internal set; } // set by persistence + public DateTime? PublishDate { get; set; } // set by persistence /// [IgnoreDataMember] - public int? PublisherId { get; internal set; } // set by persistence + public int? PublisherId { get; set; } // set by persistence /// [IgnoreDataMember] - public int? PublishTemplateId { get; internal set; } // set by persistence + public int? PublishTemplateId { get; set; } // set by persistence /// [IgnoreDataMember] - public string PublishName { get; internal set; } // set by persistence + public string PublishName { get; set; } // set by persistence /// [IgnoreDataMember] - public IEnumerable EditedCultures => CultureInfos.Keys.Where(IsCultureEdited); + public IEnumerable EditedCultures + { + get => CultureInfos.Keys.Where(IsCultureEdited); + set => _editedCultures = value == null ? null : new HashSet(value, StringComparer.OrdinalIgnoreCase); + } /// [IgnoreDataMember] @@ -193,32 +204,6 @@ namespace Umbraco.Core.Models // a non-available culture could not become published anyways => _publishInfos != null && _publishInfos.ContainsKey(culture); - /// - public bool WasCulturePublished(string culture) - // just check _publishInfosOrig - a copy of _publishInfos - // a non-available culture could not become published anyways - => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); - - // adjust dates to sync between version, cultures etc - // used by the repo when persisting - internal void AdjustDates(DateTime date) - { - foreach (var culture in PublishedCultures.ToList()) - { - if (_publishInfos == null || !_publishInfos.TryGetValue(culture, out var publishInfos)) - continue; - - if (_publishInfosOrig != null && _publishInfosOrig.TryGetValue(culture, out var publishInfosOrig) - && publishInfosOrig.Date == publishInfos.Date) - continue; - - _publishInfos.AddOrUpdate(culture, publishInfos.Name, date); - - if (CultureInfos.TryGetValue(culture, out var infos)) - SetCultureInfo(culture, infos.Name, date); - } - } - /// public bool IsCultureEdited(string culture) => IsCultureAvailable(culture) && // is available, and @@ -227,7 +212,23 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - public IReadOnlyDictionary PublishCultureInfos => _publishInfos ?? NoInfos; + public ContentCultureInfosCollection PublishCultureInfos + { + get + { + if (_publishInfos != null) return _publishInfos; + _publishInfos = new ContentCultureInfosCollection(); + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + return _publishInfos; + } + set + { + if (_publishInfos != null) _publishInfos.CollectionChanged -= PublishNamesCollectionChanged; + _publishInfos = value; + if (_publishInfos != null) + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + } + } /// public string GetPublishName(string culture) @@ -244,67 +245,7 @@ namespace Umbraco.Core.Models if (culture.IsNullOrWhiteSpace()) return PublishDate; if (!ContentType.VariesByCulture()) return null; if (_publishInfos == null) return null; - return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; - } - - // internal for repository - internal void SetPublishInfo(string culture, string name, DateTime date) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); - - if (culture.IsNullOrWhiteSpace()) - throw new ArgumentNullOrEmptyException(nameof(culture)); - - if (_publishInfos == null) - { - _publishInfos = new ContentCultureInfosCollection(); - _publishInfos.CollectionChanged += PublishNamesCollectionChanged; - } - - _publishInfos.AddOrUpdate(culture, name, date); - } - - private void ClearPublishInfos() - { - _publishInfos = null; - } - - private void ClearPublishInfo(string culture) - { - if (culture.IsNullOrWhiteSpace()) - throw new ArgumentNullOrEmptyException(nameof(culture)); - - if (_publishInfos == null) return; - _publishInfos.Remove(culture); - if (_publishInfos.Count == 0) _publishInfos = null; - - // set the culture to be dirty - it's been modified - TouchCultureInfo(culture); - } - - // sets a publish edited - internal void SetCultureEdited(string culture) - { - if (culture.IsNullOrWhiteSpace()) - throw new ArgumentNullOrEmptyException(nameof(culture)); - if (_editedCultures == null) - _editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); - _editedCultures.Add(culture.ToLowerInvariant()); - } - - // sets all publish edited - internal void SetCultureEdited(IEnumerable cultures) - { - if (cultures == null) - { - _editedCultures = null; - } - else - { - var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase); - _editedCultures = editedCultures.Count > 0 ? editedCultures : null; - } + return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?)null; } /// @@ -313,94 +254,54 @@ namespace Umbraco.Core.Models private void PublishNamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(PublishCultureInfos)); + + //we don't need to handle other actions, only add/remove, however we could implement Replace and track updated cultures in _updatedCultures too + //which would allows us to continue doing WasCulturePublished, but don't think we need it anymore + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + var cultureInfo = e.NewItems.Cast().First(); + if (_currentPublishCultureChanges.addedCultures == null) _currentPublishCultureChanges.addedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + _currentPublishCultureChanges.addedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.removedCultures?.Remove(cultureInfo.Culture); + break; + } + case NotifyCollectionChangedAction.Remove: + { + //remove listening for changes + var cultureInfo = e.OldItems.Cast().First(); + if (_currentPublishCultureChanges.removedCultures == null) _currentPublishCultureChanges.removedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + _currentPublishCultureChanges.removedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); + _currentPublishCultureChanges.addedCultures?.Remove(cultureInfo.Culture); + break; + } + case NotifyCollectionChangedAction.Replace: + { + //replace occurs when an Update occurs + var cultureInfo = e.NewItems.Cast().First(); + if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); + break; + } + } } [IgnoreDataMember] - public int PublishedVersionId { get; internal set; } + public int PublishedVersionId { get; set; } [DataMember] - public bool Blueprint { get; internal set; } - - /// - public bool PublishCulture(string culture = "*") - { - culture = culture.NullOrWhiteSpaceAsNull(); - - // the variation should be supported by the content type properties - // if the content type is invariant, only '*' and 'null' is ok - // if the content type varies, everything is ok because some properties may be invariant - if (!ContentType.SupportsPropertyVariation(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); - - // the values we want to publish should be valid - if (ValidateProperties(culture).Any()) - return false; - - var alsoInvariant = false; - if (culture == "*") // all cultures - { - foreach (var c in AvailableCultures) - { - var name = GetCultureName(c); - if (string.IsNullOrWhiteSpace(name)) - return false; - SetPublishInfo(c, name, DateTime.Now); - } - } - else if (culture == null) // invariant culture - { - if (string.IsNullOrWhiteSpace(Name)) - return false; - // PublishName set by repository - nothing to do here - } - else // one single culture - { - var name = GetCultureName(culture); - if (string.IsNullOrWhiteSpace(name)) - return false; - SetPublishInfo(culture, name, DateTime.Now); - alsoInvariant = true; // we also want to publish invariant values - } - - // property.PublishValues only publishes what is valid, variation-wise - foreach (var property in Properties) - { - property.PublishValues(culture); - if (alsoInvariant) - property.PublishValues(null); - } - - _publishedState = PublishedState.Publishing; - return true; - } - - /// - public void UnpublishCulture(string culture = "*") - { - culture = culture.NullOrWhiteSpaceAsNull(); - - // the variation should be supported by the content type properties - if (!ContentType.SupportsPropertyVariation(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); - - if (culture == "*") // all cultures - ClearPublishInfos(); - else // one single culture - ClearPublishInfo(culture); - - // property.PublishValues only publishes what is valid, variation-wise - foreach (var property in Properties) - property.UnpublishValues(culture); - - _publishedState = PublishedState.Publishing; - } + public bool Blueprint { get; set; } /// /// Changes the for the current content object /// /// New ContentType for this content /// Leaves PropertyTypes intact after change - public void ChangeContentType(IContentType contentType) + internal void ChangeContentType(IContentType contentType) { ChangeContentType(contentType, false); } @@ -411,7 +312,7 @@ namespace Umbraco.Core.Models /// /// New ContentType for this content /// Boolean indicating whether to clear PropertyTypes upon change - public void ChangeContentType(IContentType contentType, bool clearProperties) + internal void ChangeContentType(IContentType contentType, bool clearProperties) { ChangeContentType(new SimpleContentType(contentType)); @@ -424,25 +325,91 @@ namespace Umbraco.Core.Models Properties.CollectionChanged += PropertiesChanged; } + public override void ResetWereDirtyProperties() + { + base.ResetWereDirtyProperties(); + _previousPublishCultureChanges.updatedCultures = null; + _previousPublishCultureChanges.removedCultures = null; + _previousPublishCultureChanges.addedCultures = null; + } + public override void ResetDirtyProperties(bool rememberDirty) { base.ResetDirtyProperties(rememberDirty); + if (rememberDirty) + { + _previousPublishCultureChanges.addedCultures = _currentPublishCultureChanges.addedCultures == null || _currentPublishCultureChanges.addedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousPublishCultureChanges.removedCultures = _currentPublishCultureChanges.removedCultures == null || _currentPublishCultureChanges.removedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousPublishCultureChanges.updatedCultures = _currentPublishCultureChanges.updatedCultures == null || _currentPublishCultureChanges.updatedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); + } + else + { + _previousPublishCultureChanges.addedCultures = null; + _previousPublishCultureChanges.removedCultures = null; + _previousPublishCultureChanges.updatedCultures = null; + } + _currentPublishCultureChanges.addedCultures?.Clear(); + _currentPublishCultureChanges.removedCultures?.Clear(); + _currentPublishCultureChanges.updatedCultures?.Clear(); + // take care of the published state _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; - // Make a copy of the _publishInfos, this is purely so that we can detect - // if this entity's previous culture publish state (regardless of the rememberDirty flag) - _publishInfosOrig = _publishInfos == null - ? null - : new ContentCultureInfosCollection(_publishInfos); - if (_publishInfos == null) return; foreach (var infos in _publishInfos) infos.ResetDirtyProperties(rememberDirty); } + /// + /// Overridden to check special keys. + public override bool IsPropertyDirty(string propertyName) + { + //Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith("_publishedCulture_")) + { + var culture = propertyName.TrimStart("_publishedCulture_"); + return _currentPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_unpublishedCulture_")) + { + var culture = propertyName.TrimStart("_unpublishedCulture_"); + return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_changedCulture_")) + { + var culture = propertyName.TrimStart("_changedCulture_"); + return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return base.IsPropertyDirty(propertyName); + } + + /// + /// Overridden to check special keys. + public override bool WasPropertyDirty(string propertyName) + { + //Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith("_publishedCulture_")) + { + var culture = propertyName.TrimStart("_publishedCulture_"); + return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_unpublishedCulture_")) + { + var culture = propertyName.TrimStart("_unpublishedCulture_"); + return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_changedCulture_")) + { + var culture = propertyName.TrimStart("_changedCulture_"); + return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return base.WasPropertyDirty(propertyName); + } + /// /// Creates a deep clone of the current entity with its identity and it's property identities reset /// @@ -466,11 +433,13 @@ namespace Umbraco.Core.Models var clonedContent = (Content)clone; + //fixme - need to reset change tracking bits + //if culture infos exist then deal with event bindings if (clonedContent._publishInfos != null) { clonedContent._publishInfos.CollectionChanged -= PublishNamesCollectionChanged; //clear this event handler if any - clonedContent._publishInfos = (ContentCultureInfosCollection) _publishInfos.DeepClone(); //manually deep clone + clonedContent._publishInfos = (ContentCultureInfosCollection)_publishInfos.DeepClone(); //manually deep clone clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler } @@ -481,6 +450,14 @@ namespace Umbraco.Core.Models 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; + + clonedContent._previousPublishCultureChanges.updatedCultures = null; + clonedContent._previousPublishCultureChanges.addedCultures = null; + clonedContent._previousPublishCultureChanges.removedCultures = null; } } } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 765275cf51..3a372296f0 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -18,13 +18,19 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentType.Alias}")] public abstract class ContentBase : TreeEntityBase, IContentBase { - protected static readonly ContentCultureInfosCollection NoInfos = new ContentCultureInfosCollection(); private int _contentTypeId; private int _writerId; private PropertyCollection _properties; private ContentCultureInfosCollection _cultureInfos; + #region Used for change tracking + + private (HashSet addedCultures, HashSet removedCultures, HashSet updatedCultures) _currentCultureChanges; + private (HashSet addedCultures, HashSet removedCultures, HashSet updatedCultures) _previousCultureChanges; + + #endregion + /// /// Initializes a new instance of the class. /// @@ -74,24 +80,26 @@ namespace Umbraco.Core.Models OnPropertyChanged(nameof(Properties)); } + + /// /// Id of the user who wrote/updated this entity /// [DataMember] - public virtual int WriterId + public int WriterId { get => _writerId; set => SetPropertyValueAndDetectChanges(value, ref _writerId, nameof(WriterId)); } [IgnoreDataMember] - public int VersionId { get; internal set; } + public int VersionId { get; set; } /// /// Integer Id of the default ContentType /// [DataMember] - public virtual int ContentTypeId + public int ContentTypeId { get { @@ -114,11 +122,12 @@ namespace Umbraco.Core.Models /// [DataMember] [DoNotClone] - public virtual PropertyCollection Properties + public PropertyCollection Properties { get => _properties; set { + if (_properties != null) _properties.CollectionChanged -= PropertiesChanged; _properties = value; _properties.CollectionChanged += PropertiesChanged; } @@ -142,7 +151,23 @@ namespace Umbraco.Core.Models /// [DataMember] - public virtual IReadOnlyDictionary CultureInfos => _cultureInfos ?? NoInfos; + public ContentCultureInfosCollection CultureInfos + { + get + { + if (_cultureInfos != null) return _cultureInfos; + _cultureInfos = new ContentCultureInfosCollection(); + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; + return _cultureInfos; + } + set + { + if (_cultureInfos != null) _cultureInfos.CollectionChanged -= CultureInfosCollectionChanged; + _cultureInfos = value; + if (_cultureInfos != null) + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; + } + } /// public string GetCultureName(string culture) @@ -159,7 +184,7 @@ namespace Umbraco.Core.Models if (culture.IsNullOrWhiteSpace()) return null; if (!ContentType.VariesByCulture()) return null; if (_cultureInfos == null) return null; - return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; + return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?)null; } /// @@ -177,7 +202,7 @@ namespace Umbraco.Core.Models } else // set { - SetCultureInfo(culture, name, DateTime.Now); + this.SetCultureInfo(culture, name, DateTime.Now); } } else // set on invariant content type @@ -189,13 +214,7 @@ namespace Umbraco.Core.Models } } - protected void ClearCultureInfos() - { - _cultureInfos?.Clear(); - _cultureInfos = null; - } - - protected void ClearCultureInfo(string culture) + private void ClearCultureInfo(string culture) { if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); @@ -206,36 +225,44 @@ namespace Umbraco.Core.Models _cultureInfos = null; } - protected void TouchCultureInfo(string culture) - { - if (_cultureInfos == null || !_cultureInfos.TryGetValue(culture, out var infos)) return; - _cultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now); - } - - // internal for repository - internal void SetCultureInfo(string culture, string name, DateTime date) - { - if (name.IsNullOrWhiteSpace()) - throw new ArgumentNullOrEmptyException(nameof(name)); - - if (culture.IsNullOrWhiteSpace()) - throw new ArgumentNullOrEmptyException(nameof(culture)); - - if (_cultureInfos == null) - { - _cultureInfos = new ContentCultureInfosCollection(); - _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; - } - - _cultureInfos.AddOrUpdate(culture, name, date); - } - /// /// Handles culture infos collection changes. /// private void CultureInfosCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(CultureInfos)); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + var cultureInfo = e.NewItems.Cast().First(); + if (_currentCultureChanges.addedCultures == null) _currentCultureChanges.addedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + _currentCultureChanges.addedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.removedCultures?.Remove(cultureInfo.Culture); + break; + } + case NotifyCollectionChangedAction.Remove: + { + //remove listening for changes + var cultureInfo = e.OldItems.Cast().First(); + if (_currentCultureChanges.removedCultures == null) _currentCultureChanges.removedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + _currentCultureChanges.removedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); + _currentCultureChanges.addedCultures?.Remove(cultureInfo.Culture); + break; + } + case NotifyCollectionChangedAction.Replace: + { + //replace occurs when an Update occurs + var cultureInfo = e.NewItems.Cast().First(); + if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); + _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); + break; + } + } } #endregion @@ -243,11 +270,11 @@ namespace Umbraco.Core.Models #region Has, Get, Set, Publish Property Value /// - public virtual bool HasProperty(string propertyTypeAlias) + public bool HasProperty(string propertyTypeAlias) => Properties.Contains(propertyTypeAlias); /// - public virtual object GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) + public object GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) { return Properties.TryGetValue(propertyTypeAlias, out var property) ? property.GetValue(culture, segment, published) @@ -255,7 +282,7 @@ namespace Umbraco.Core.Models } /// - public virtual TValue GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) + public TValue GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) { if (!Properties.TryGetValue(propertyTypeAlias, out var property)) return default; @@ -265,11 +292,13 @@ namespace Umbraco.Core.Models } /// - public virtual void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null) + public void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null) { if (Properties.Contains(propertyTypeAlias)) { Properties[propertyTypeAlias].SetValue(value, culture, segment); + //bump the culture to be flagged for updating + this.TouchCulture(culture); return; } @@ -281,120 +310,45 @@ namespace Umbraco.Core.Models var property = propertyType.CreateProperty(); property.SetValue(value, culture, segment); Properties.Add(property); - } - #endregion - - #region Copy - - /// - public virtual void CopyFrom(IContent other, string culture = "*") - { - if (other.ContentTypeId != ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); - - // the variation should be supported by the content type properties - // if the content type is invariant, only '*' and 'null' is ok - // if the content type varies, everything is ok because some properties may be invariant - if (!ContentType.SupportsPropertyVariation(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); - - // copying from the same Id and VersionPk - var copyingFromSelf = Id == other.Id && VersionId == other.VersionId; - var published = copyingFromSelf; - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties for the specified culture - foreach (var property in Properties) - { - // each property type may or may not support the variation - if (!property.PropertyType.SupportsVariation(culture, "*", wildcards: true)) - continue; - - foreach (var pvalue in property.Values) - if (property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && - (culture == "*" || pvalue.Culture.InvariantEquals(culture))) - { - property.SetValue(null, pvalue.Culture, pvalue.Segment); - } - } - - // copy properties from 'other' - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - if (!otherProperty.PropertyType.SupportsVariation(culture, "*", wildcards: true)) - continue; - - var alias = otherProperty.PropertyType.Alias; - foreach (var pvalue in otherProperty.Values) - { - if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && - (culture == "*" || pvalue.Culture.InvariantEquals(culture))) - { - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - SetValue(alias, value, pvalue.Culture, pvalue.Segment); - } - } - } - - // copy names, too - - if (culture == "*") - ClearCultureInfos(); - - if (culture == null || culture == "*") - Name = other.Name; - - foreach (var (otherCulture, otherInfos) in other.CultureInfos) - { - if (culture == "*" || culture == otherCulture) - SetCultureName(otherInfos.Name, otherCulture); - } - } - - #endregion - - #region Validation - - /// - public virtual Property[] ValidateProperties(string culture = "*") - { - // select invalid properties - return Properties.Where(x => - { - // if culture is null, we validate invariant properties only - // if culture is '*' we validate both variant and invariant properties, automatically - // if culture is specific eg 'en-US' we both too, but explicitly - - var varies = x.PropertyType.VariesByCulture(); - - if (culture == null) - return !(varies || x.IsValid(null)); // validate invariant property, invariant culture - - if (culture == "*") - return !x.IsValid(culture); // validate property, all cultures - - return varies - ? !x.IsValid(culture) // validate variant property, explicit culture - : !x.IsValid(null); // validate invariant property, explicit culture - }) - .ToArray(); + //bump the culture to be flagged for updating + this.TouchCulture(culture); } #endregion #region Dirty + public override void ResetWereDirtyProperties() + { + base.ResetWereDirtyProperties(); + _previousCultureChanges.addedCultures = null; + _previousCultureChanges.removedCultures = null; + _previousCultureChanges.updatedCultures = null; + } + /// /// Overridden to include user properties. public override void ResetDirtyProperties(bool rememberDirty) { base.ResetDirtyProperties(rememberDirty); + if (rememberDirty) + { + _previousCultureChanges.addedCultures = _currentCultureChanges.addedCultures == null || _currentCultureChanges.addedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousCultureChanges.removedCultures = _currentCultureChanges.removedCultures == null || _currentCultureChanges.removedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousCultureChanges.updatedCultures = _currentCultureChanges.updatedCultures == null || _currentCultureChanges.updatedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); + } + else + { + _previousCultureChanges.addedCultures = null; + _previousCultureChanges.removedCultures = null; + _previousCultureChanges.updatedCultures = null; + } + _currentCultureChanges.addedCultures?.Clear(); + _currentCultureChanges.removedCultures?.Clear(); + _currentCultureChanges.updatedCultures?.Clear(); + // also reset dirty changes made to user's properties foreach (var prop in Properties) prop.ResetDirtyProperties(rememberDirty); @@ -443,6 +397,23 @@ namespace Umbraco.Core.Models if (base.IsPropertyDirty(propertyName)) return true; + //Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith("_addedCulture_")) + { + var culture = propertyName.TrimStart("_addedCulture_"); + return _currentCultureChanges.addedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_removedCulture_")) + { + var culture = propertyName.TrimStart("_removedCulture_"); + return _currentCultureChanges.removedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_updatedCulture_")) + { + var culture = propertyName.TrimStart("_updatedCulture_"); + return _currentCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + return Properties.Contains(propertyName) && Properties[propertyName].IsDirty(); } @@ -453,6 +424,23 @@ namespace Umbraco.Core.Models if (base.WasPropertyDirty(propertyName)) return true; + //Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith("_addedCulture_")) + { + var culture = propertyName.TrimStart("_addedCulture_"); + return _previousCultureChanges.addedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_removedCulture_")) + { + var culture = propertyName.TrimStart("_removedCulture_"); + return _previousCultureChanges.removedCultures?.Contains(culture) ?? false; + } + if (propertyName.StartsWith("_updatedCulture_")) + { + var culture = propertyName.TrimStart("_updatedCulture_"); + return _previousCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + return Properties.Contains(propertyName) && Properties[propertyName].WasDirty(); } @@ -493,7 +481,7 @@ namespace Umbraco.Core.Models if (clonedContent._cultureInfos != null) { clonedContent._cultureInfos.CollectionChanged -= CultureInfosCollectionChanged; //clear this event handler if any - clonedContent._cultureInfos = (ContentCultureInfosCollection) _cultureInfos.DeepClone(); //manually deep clone + clonedContent._cultureInfos = (ContentCultureInfosCollection)_cultureInfos.DeepClone(); //manually deep clone clonedContent._cultureInfos.CollectionChanged += clonedContent.CultureInfosCollectionChanged; //re-assign correct event handler } @@ -501,9 +489,17 @@ namespace Umbraco.Core.Models if (clonedContent._properties != null) { clonedContent._properties.CollectionChanged -= PropertiesChanged; //clear this event handler if any - clonedContent._properties = (PropertyCollection) _properties.DeepClone(); //manually deep clone + clonedContent._properties = (PropertyCollection)_properties.DeepClone(); //manually deep clone clonedContent._properties.CollectionChanged += clonedContent.PropertiesChanged; //re-assign correct event handler } + + clonedContent._currentCultureChanges.updatedCultures = null; + clonedContent._currentCultureChanges.addedCultures = null; + clonedContent._currentCultureChanges.removedCultures = null; + + clonedContent._previousCultureChanges.updatedCultures = null; + clonedContent._previousCultureChanges.addedCultures = null; + clonedContent._previousCultureChanges.removedCultures = null; } } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs index 82b0ba6475..52f6f9adb6 100644 --- a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs +++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs @@ -17,20 +17,6 @@ namespace Umbraco.Core.Models public ContentCultureInfosCollection() : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) { } - - /// - /// Initializes a new instance of the class with items. - /// - public ContentCultureInfosCollection(IEnumerable items) - : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) - { - // make sure to add *copies* and not the original items, - // as items can be modified by AddOrUpdate, and therefore - // the new collection would be impacted by changes made - // to the old collection - foreach (var item in items) - Add(new ContentCultureInfos(item)); - } /// /// Adds or updates a instance. @@ -53,7 +39,7 @@ namespace Umbraco.Core.Models Name = name, Date = date }); - } + } } /// diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs new file mode 100644 index 0000000000..ca9bf57902 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Exceptions; + +namespace Umbraco.Core.Models +{ + /// + /// Extension methods used to manipulate content variations by the document repository + /// + internal static class ContentRepositoryExtensions + { + /// + /// Gets the cultures that have been flagged for unpublishing. + /// + /// Gets cultures for which content.UnpublishCulture() has been invoked. + public static IReadOnlyList GetCulturesUnpublishing(this IContent content) + { + if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos")) + return Array.Empty(); + + var culturesUnpublishing = content.CultureInfos.Values + .Where(x => content.IsPropertyDirty("_unpublishedCulture_" + x.Culture)) + .Select(x => x.Culture); + + return culturesUnpublishing.ToList(); + } + + /// + /// Copies values from another document. + /// + public static void CopyFrom(this IContent content, IContent other, string culture = "*") + { + if (other.ContentTypeId != content.ContentTypeId) + throw new InvalidOperationException("Cannot copy values from a different content type."); + + culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + + // copying from the same Id and VersionPk + var copyingFromSelf = content.Id == other.Id && content.VersionId == other.VersionId; + var published = copyingFromSelf; + + // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails + + // clear all existing properties for the specified culture + foreach (var property in content.Properties) + { + // each property type may or may not support the variation + if (!property.PropertyType.SupportsVariation(culture, "*", wildcards: true)) + continue; + + foreach (var pvalue in property.Values) + if (property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && + (culture == "*" || pvalue.Culture.InvariantEquals(culture))) + { + property.SetValue(null, pvalue.Culture, pvalue.Segment); + } + } + + // copy properties from 'other' + var otherProperties = other.Properties; + foreach (var otherProperty in otherProperties) + { + if (!otherProperty.PropertyType.SupportsVariation(culture, "*", wildcards: true)) + continue; + + var alias = otherProperty.PropertyType.Alias; + foreach (var pvalue in otherProperty.Values) + { + if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && + (culture == "*" || pvalue.Culture.InvariantEquals(culture))) + { + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + content.SetValue(alias, value, pvalue.Culture, pvalue.Segment); + } + } + } + + // copy names, too + + if (culture == "*") + { + content.CultureInfos.Clear(); + content.CultureInfos = null; + } + + + if (culture == null || culture == "*") + content.Name = other.Name; + + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in other.CultureInfos) + { + if (culture == "*" || culture == cultureInfo.Culture) + content.SetCultureName(cultureInfo.Name, cultureInfo.Culture); + } + } + + /// + /// Validates the content item's properties pass variant rules + /// + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor + /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. + public static Property[] ValidateProperties(this IContentBase content, string culture = "*") + { + // select invalid properties + return content.Properties.Where(x => + { + // if culture is null, we validate invariant properties only + // if culture is '*' we validate both variant and invariant properties, automatically + // if culture is specific eg 'en-US' we both too, but explicitly + + var varies = x.PropertyType.VariesByCulture(); + + if (culture == null) + return !(varies || x.IsValid(null)); // validate invariant property, invariant culture + + if (culture == "*") + return !x.IsValid(culture); // validate property, all cultures + + return varies + ? !x.IsValid(culture) // validate variant property, explicit culture + : !x.IsValid(null); // validate invariant property, explicit culture + }) + .ToArray(); + } + + public static void SetPublishInfo(this IContent content, string culture, string name, DateTime date) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + content.PublishCultureInfos.AddOrUpdate(culture, name, date); + } + + /// + /// Used to synchronize all culture dates to the same date if they've been modified + /// + /// + /// + /// + /// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible that + /// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact same time. + /// + public static void AdjustDates(this IContent content, DateTime date) + { + foreach (var culture in content.PublishedCultures.ToList()) + { + if (!content.PublishCultureInfos.TryGetValue(culture, out var publishInfos)) + continue; + + if (!publishInfos.IsDirty()) + continue; //if it's not dirty, it means it hasn't changed so there's nothing to adjust + + content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date); + + if (content.CultureInfos.TryGetValue(culture, out var infos)) + SetCultureInfo(content, culture, infos.Name, date); + } + } + + // sets the edited cultures on the content + public static void SetCultureEdited(this IContent content, IEnumerable cultures) + { + if (cultures == null) + content.EditedCultures = null; + else + { + var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase); + content.EditedCultures = editedCultures.Count > 0 ? editedCultures : null; + } + } + + public static void SetCultureInfo(this IContentBase content, string culture, string name, DateTime date) + { + if (name.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + content.CultureInfos.AddOrUpdate(culture, name, date); + } + + public static bool PublishCulture(this IContent content, string culture = "*") + { + culture = culture.NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + + // the values we want to publish should be valid + if (content.ValidateProperties(culture).Any()) + return false; + + var alsoInvariant = false; + if (culture == "*") // all cultures + { + foreach (var c in content.AvailableCultures) + { + var name = content.GetCultureName(c); + if (string.IsNullOrWhiteSpace(name)) + return false; + content.SetPublishInfo(c, name, DateTime.Now); + } + } + else if (culture == null) // invariant culture + { + if (string.IsNullOrWhiteSpace(content.Name)) + return false; + // PublishName set by repository - nothing to do here + } + else // one single culture + { + var name = content.GetCultureName(culture); + if (string.IsNullOrWhiteSpace(name)) + return false; + content.SetPublishInfo(culture, name, DateTime.Now); + alsoInvariant = true; // we also want to publish invariant values + } + + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in content.Properties) + { + property.PublishValues(culture); + if (alsoInvariant) + property.PublishValues(null); + } + + content.PublishedState = PublishedState.Publishing; + return true; + } + + public static void UnpublishCulture(this IContent content, string culture = "*") + { + culture = culture.NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + + if (culture == "*") // all cultures + content.ClearPublishInfos(); + else // one single culture + content.ClearPublishInfo(culture); + + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in content.Properties) + property.UnpublishValues(culture); + + content.PublishedState = PublishedState.Publishing; + } + + public static void ClearPublishInfos(this IContent content) + { + content.PublishCultureInfos = null; + } + + public static void ClearPublishInfo(this IContent content, string culture) + { + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + content.PublishCultureInfos.Remove(culture); + + // set the culture to be dirty - it's been modified + content.TouchCulture(culture); + } + + /// + /// Updates a culture date, if the culture exists. + /// + public static void TouchCulture(this IContentBase content, string culture) + { + if (culture.IsNullOrWhiteSpace()) return; + if (!content.CultureInfos.TryGetValue(culture, out var infos)) return; + content.CultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now); + } + } +} diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs index a598ba1b3d..a1f4bad9a1 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs @@ -65,7 +65,7 @@ namespace Umbraco.Core.Models.Entities } /// - public void ResetWereDirtyProperties() + public virtual void ResetWereDirtyProperties() { // note: cannot .Clear() because when memberwise-cloning this will be the SAME // instance as the one on the clone, so we need to create a new instance. diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 1e1867ddc6..6990a7f7da 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -25,46 +25,46 @@ namespace Umbraco.Core.Models /// /// Gets a value indicating whether the content is published. /// - bool Published { get; } + bool Published { get; set; } - PublishedState PublishedState { get; } + PublishedState PublishedState { get; set; } /// /// Gets a value indicating whether the content has been edited. /// - bool Edited { get; } + bool Edited { get; set; } /// /// Gets the published version identifier. /// - int PublishedVersionId { get; } + int PublishedVersionId { get; set; } /// /// Gets a value indicating whether the content item is a blueprint. /// - bool Blueprint { get; } + bool Blueprint { get; set; } /// /// Gets the template id used to render the published version of the content. /// /// When editing the content, the template can change, but this will not until the content is published. - int? PublishTemplateId { get; } + int? PublishTemplateId { get; set; } /// /// Gets the name of the published version of the content. /// /// When editing the content, the name can change, but this will not until the content is published. - string PublishName { get; } + string PublishName { get; set; } /// /// Gets the identifier of the user who published the content. /// - int? PublisherId { get; } + int? PublisherId { get; set; } /// /// Gets the date and time the content was published. /// - DateTime? PublishDate { get; } + DateTime? PublishDate { get; set; } /// /// Gets a value indicating whether a culture is published. @@ -74,19 +74,11 @@ namespace Umbraco.Core.Models /// and the content published name for this culture is non-null. It becomes non-published /// whenever values for this culture are unpublished. /// A culture becomes published as soon as PublishCulture has been invoked, - /// even though the document might now have been saved yet (and can have no identity). + /// even though the document might not have been saved yet (and can have no identity). /// Does not support the '*' wildcard (returns false). /// bool IsCulturePublished(string culture); - /// - /// Gets a value indicating whether a culture was published. - /// - /// - /// Mirrors whenever the content item is saved. - /// - bool WasCulturePublished(string culture); - /// /// Gets the date a culture was published. /// @@ -120,7 +112,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get via the property. /// - IReadOnlyDictionary PublishCultureInfos { get; } + ContentCultureInfosCollection PublishCultureInfos { get; set; } /// /// Gets the published cultures. @@ -130,47 +122,13 @@ namespace Umbraco.Core.Models /// /// Gets the edited cultures. /// - IEnumerable EditedCultures { get; } - - // TODO: these two should move to some kind of service - - /// - /// Changes the for the current content object - /// - /// New ContentType for this content - /// Leaves PropertyTypes intact after change - void ChangeContentType(IContentType contentType); - - /// - /// Changes the for the current content object and removes PropertyTypes, - /// which are not part of the new ContentType. - /// - /// New ContentType for this content - /// Boolean indicating whether to clear PropertyTypes upon change - void ChangeContentType(IContentType contentType, bool clearProperties); + IEnumerable EditedCultures { get; set; } /// /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset /// /// IContent DeepCloneWithResetIdentities(); - - /// - /// Registers a culture to be published. - /// - /// A value indicating whether the culture can be published. - /// - /// Fails if properties don't pass variant validation rules. - /// Publishing must be finalized via the content service SavePublishing method. - /// - bool PublishCulture(string culture = "*"); - - /// - /// Registers a culture to be unpublished. - /// - /// - /// Unpublishing must be finalized via the content service SavePublishing method. - /// - void UnpublishCulture(string culture = "*"); + } } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 1ed042d5bd..8df55bc5a4 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -31,7 +31,7 @@ namespace Umbraco.Core.Models /// /// Gets the version identifier. /// - int VersionId { get; } + int VersionId { get; set; } /// /// Sets the name of the content item for a specified culture. @@ -62,8 +62,8 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot contain the invariant /// culture name, which must be get or set via the property. /// - IReadOnlyDictionary CultureInfos { get; } - + ContentCultureInfosCollection CultureInfos { get; set; } + /// /// Gets the available cultures. /// @@ -125,16 +125,5 @@ namespace Umbraco.Core.Models /// Values 'null' and 'empty' are equivalent for culture and segment. void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null); - /// - /// Copies values from another document. - /// - void CopyFrom(IContent other, string culture = "*"); - - /// - /// Validates the content item's properties pass variant rules - /// - /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor - /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. - Property[] ValidateProperties(string culture = "*"); } } diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index bc71d3dc2b..4f9e79f482 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -142,13 +142,15 @@ namespace Umbraco.Core.Models } public override void ResetDirtyProperties(bool rememberDirty) - { + { + base.ResetDirtyProperties(rememberDirty); + _addedProperties.Clear(); _removedProperties.Clear(); - base.ResetDirtyProperties(rememberDirty); + foreach (var prop in Properties) { - ((BeingDirtyBase)prop).ResetDirtyProperties(rememberDirty); + prop.ResetDirtyProperties(rememberDirty); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index abf4156d7f..8a44a7a9b4 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -70,9 +70,7 @@ namespace Umbraco.Core.Models.PublishedContent if (_publishedDataTypes == null) { var dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary( - x => x.Id, - x => new PublishedDataType(x.Id, x.EditorAlias, x is DataType d ? d.GetLazyConfiguration() : new Lazy(() => x.Configuration))); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); } publishedDataTypes = _publishedDataTypes; @@ -89,12 +87,12 @@ namespace Umbraco.Core.Models.PublishedContent { lock (_publishedDataTypesLocker) { - foreach (var id in ids) - _publishedDataTypes.Remove(id); var dataTypes = _dataTypeService.GetAll(ids); - foreach (var dataType in dataTypes) - _publishedDataTypes[dataType.Id] = new PublishedDataType(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfiguration() : new Lazy(() => dataType.Configuration)); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); } } + + private PublishedDataType CreatePublishedDataType(IDataType dataType) + => new PublishedDataType(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfiguration() : new Lazy(() => dataType.Configuration)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index fc5382499f..d49c92f1bf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -7,6 +7,8 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IDocumentRepository : IContentRepository, IReadRepository { + + /// /// 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.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 6af7031883..04af52047c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -120,14 +120,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement break; case QueryType.Single: case QueryType.Many: - sql = sql.Select(r => - r.Select(documentDto => documentDto.ContentDto, r1 => - r1.Select(contentDto => contentDto.NodeDto)) - .Select(documentDto => documentDto.DocumentVersionDto, r1 => - r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) - .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => - r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto, "pcv"))) + //we've put this in a local function so that the below sql.Select statement doesn't have a problem + //thinking that the call is ambiguous + NPocoSqlExtensions.SqlRef SelectStatement(NPocoSqlExtensions.SqlRef r) => + r.Select(documentDto => documentDto.ContentDto, r1 => r1.Select(contentDto => contentDto.NodeDto)) + .Select(documentDto => documentDto.DocumentVersionDto, r1 => r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) + .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto, "pcv")); + + sql = sql.Select(SelectStatement) // select the variant name, coalesce to the invariant name, as "variantName" .AndSelect(VariantNameSqlExpression + " AS variantName"); break; @@ -380,9 +381,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.AdjustDates(contentVersionDto.VersionDate); // names also impact 'edited' - foreach (var (culture, infos) in content.CultureInfos) - if (infos.Name != content.GetPublishName(culture)) - (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in content.CultureInfos) + if (cultureInfo.Name != content.GetPublishName(cultureInfo.Culture)) + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); // insert content variations Database.BulkInsertRecords(GetContentVariationDtos(content, publishing)); @@ -541,11 +543,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.AdjustDates(contentVersionDto.VersionDate); // names also impact 'edited' - foreach (var (culture, infos) in content.CultureInfos) - if (infos.Name != content.GetPublishName(culture)) + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in content.CultureInfos) + if (cultureInfo.Name != content.GetPublishName(cultureInfo.Culture)) { edited = true; - (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); // TODO: change tracking // at the moment, we don't do any dirty tracking on property values, so we don't know whether the @@ -951,6 +954,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) { // note: 'updater' is the user who created the latest draft version, @@ -1127,7 +1132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: 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 publishedVersionId = dto.PublishedVersionDto?.Id ?? 0; var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); var ltemp = new List> { temp }; @@ -1182,17 +1187,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return result; } + private void SetVariations(Content content, IDictionary> contentVariations, IDictionary> documentVariations) { if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) foreach (var v in contentVariation) content.SetCultureInfo(v.Culture, v.Name, v.Date); if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) + { foreach (var v in contentVariation) content.SetPublishInfo(v.Culture, v.Name, v.Date); + } if (documentVariations.TryGetValue(content.Id, out var documentVariation)) - foreach (var v in documentVariation.Where(x => x.Edited)) - content.SetCultureEdited(v.Culture); + content.SetCultureEdited(documentVariation.Where(x => x.Edited).Select(x => x.Culture)); } private IDictionary> GetContentVariations(List> temps) @@ -1262,14 +1269,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable GetContentVariationDtos(IContent content, bool publishing) { // create dtos for the 'current' (non-published) version, all cultures - foreach (var (culture, name) in content.CultureInfos) + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in content.CultureInfos) yield return new ContentVersionCultureVariationDto { VersionId = content.VersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = culture, - Name = name.Name, - UpdateDate = content.GetUpdateDate(culture) ?? DateTime.MinValue // we *know* there is a value + LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value }; // if not publishing, we're just updating the 'current' (non-published) version, @@ -1277,14 +1285,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (!publishing) yield break; // create dtos for the 'published' version, for published cultures (those having a name) - foreach (var (culture, name) in content.PublishCultureInfos) + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in content.PublishCultureInfos) yield return new ContentVersionCultureVariationDto { VersionId = content.PublishedVersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = culture, - Name = name.Name, - UpdateDate = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value + LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = content.GetPublishDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value }; } @@ -1344,7 +1353,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement EnsureVariantNamesAreUnique(content, publishing); } - private void EnsureInvariantNameExists(Content content) + private void EnsureInvariantNameExists(IContent content) { if (content.ContentType.VariesByCulture()) { @@ -1358,7 +1367,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var defaultCulture = LanguageRepository.GetDefaultIsoCode(); content.Name = defaultCulture != null && content.CultureInfos.TryGetValue(defaultCulture, out var cultureName) ? cultureName.Name - : content.CultureInfos.First().Value.Name; + : content.CultureInfos[0].Name; } else { @@ -1368,7 +1377,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } } - private void EnsureInvariantNameIsUnique(Content content) + private void EnsureInvariantNameIsUnique(IContent content) { content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); } @@ -1405,25 +1414,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // of whether the name has changed (ie the culture has been updated) - some saving culture // fr-FR could cause culture en-UK name to change - not sure that is clean - foreach (var (culture, name) in content.CultureInfos) + foreach (var cultureInfo in content.CultureInfos) { - var langId = LanguageRepository.GetIdByIsoCode(culture); + var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); if (!langId.HasValue) continue; if (!names.TryGetValue(langId.Value, out var cultureNames)) continue; // get a unique name var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name.Name); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); - if (uniqueName == content.GetCultureName(culture)) continue; + if (uniqueName == content.GetCultureName(cultureInfo.Culture)) continue; // update the name, and the publish name if published - content.SetCultureName(uniqueName, culture); - if (publishing && content.PublishCultureInfos.ContainsKey(culture)) - content.SetPublishInfo(culture, uniqueName, DateTime.Now); + content.SetCultureName(uniqueName, cultureInfo.Culture); + if (publishing && content.PublishCultureInfos.ContainsKey(cultureInfo.Culture)) + content.SetPublishInfo(cultureInfo.Culture, uniqueName, DateTime.Now); //TODO: This is weird, this call will have already been made in the SetCultureName } } + // ReSharper disable once ClassNeverInstantiated.Local private class CultureNodeName { public int Id { get; set; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 2bad7229f2..6b2dfddaeb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IRepositoryCachePolicy CreateCachePolicy() { - // TODO: wtf are we doing with cache here? + // TODO: what are we doing with cache here? // why are we using disabled cache helper up there? // // 7.6 says: diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 9c18b3d8c3..da71cb1061 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -271,7 +271,7 @@ namespace Umbraco.Core.Runtime { _state.Level = RuntimeLevel.BootFailed; _state.Reason = RuntimeLevelReason.BootFailedOnException; - timer.Fail(); + timer?.Fail(); throw; } } @@ -287,7 +287,7 @@ namespace Umbraco.Core.Runtime } catch { - timer.Fail(); + timer?.Fail(); throw; } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 7fb7450b46..600921ee78 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -336,29 +336,32 @@ namespace Umbraco.Core.Services /// /// /// By default, publishes all variations of the document, but it is possible to specify a culture to be published. - /// When a culture is being published, it includes all varying values along with all invariant values. For - /// anything more complicated, see . + /// When a culture is being published, it includes all varying values along with all invariant values. /// The document is *always* saved, even when publishing fails. /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. /// + /// + /// + /// + /// + /// PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = 0, bool raiseEvents = true); /// - /// Saves and publishes a publishing document. + /// Saves and publishes a document. /// /// - /// A publishing document is a document with values that are being published, i.e. - /// that have been published or cleared via and - /// . - /// When one needs to publish or unpublish a single culture, or all cultures, using - /// and is the way to go. But if one needs to, say, publish two cultures and unpublish a third - /// one, in one go, then one needs to invoke and - /// on the content itself - this prepares the content, but does not commit anything - and then, invoke - /// to actually commit the changes to the database. + /// By default, publishes all variations of the document, but it is possible to specify a culture to be published. + /// When a culture is being published, it includes all varying values along with all invariant values. /// The document is *always* saved, even when publishing fails. /// - PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true); + /// + /// The cultures to publish. + /// + /// + /// + PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = 0, bool raiseEvents = true); /// /// Saves and publishes a document branch. diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index c5745090f9..ce7417722d 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -27,18 +26,16 @@ namespace Umbraco.Core.Services.Implement private readonly IContentTypeRepository _contentTypeRepository; private readonly IDocumentBlueprintRepository _documentBlueprintRepository; private readonly ILanguageRepository _languageRepository; - private readonly IMediaFileSystem _mediaFileSystem; private IQuery _queryNotTrashed; #region Constructors public ContentService(IScopeProvider provider, ILogger logger, - IEventMessagesFactory eventMessagesFactory, IMediaFileSystem mediaFileSystem, + IEventMessagesFactory eventMessagesFactory, IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository, IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository) : base(provider, logger, eventMessagesFactory) { - _mediaFileSystem = mediaFileSystem; _documentRepository = documentRepository; _entityRepository = entityRepository; _auditRepository = auditRepository; @@ -310,15 +307,16 @@ namespace Umbraco.Core.Services.Implement if (withIdentity) { + var evtMsgs = EventMessagesFactory.Get(); + // if saving is cancelled, content remains without an identity - var saveEventArgs = new SaveEventArgs(content); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) return; _documentRepository.Save(content); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved"); scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs()); } @@ -761,7 +759,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(content, evtMsgs); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) { scope.Complete(); @@ -776,7 +774,7 @@ namespace Umbraco.Core.Services.Implement //track the cultures that have changed var culturesChanging = content.ContentType.VariesByCulture() - ? content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + ? content.CultureInfos.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList() : null; // TODO: Currently there's no way to change track which variant properties have changed, we only have change // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed. @@ -787,8 +785,7 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved"); } var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); @@ -817,7 +814,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(contentsA, evtMsgs); + var saveEventArgs = new ContentSavingEventArgs(contentsA, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) { scope.Complete(); @@ -838,8 +835,7 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved"); } scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content"); @@ -857,7 +853,7 @@ namespace Umbraco.Core.Services.Implement var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method."); + throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method."); // cannot accept invariant (null or empty) culture for variant content type // cannot accept a specific culture for invariant content type (but '*' is ok) @@ -891,19 +887,45 @@ namespace Umbraco.Core.Services.Implement // finally, "save publishing" // what happens next depends on whether the content can be published or not - return SavePublishing(content, userId, raiseEvents); + return CommitDocumentChanges(content, userId, raiseEvents); + } + + /// + public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = 0, bool raiseEvents = true) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (cultures == null) throw new ArgumentNullException(nameof(cultures)); + + var evtMsgs = EventMessagesFactory.Get(); + + var varies = content.ContentType.VariesByCulture(); + + if (cultures.Length == 0) + { + //no cultures specified and doesn't vary, so publish it, else nothing to publish + return !varies + ? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents) + : new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + } + + if (cultures.Select(content.PublishCulture).Any(isValid => !isValid)) + return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); //fixme: no way to know which one failed? + + return CommitDocumentChanges(content, userId, raiseEvents); } /// public PublishResult Unpublish(IContent content, string culture = "*", int userId = 0) { + if (content == null) throw new ArgumentNullException(nameof(content)); + var evtMsgs = EventMessagesFactory.Get(); culture = culture.NullOrWhiteSpaceAsNull(); var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method."); + throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method."); // cannot accept invariant (null or empty) culture for variant content type // cannot accept a specific culture for invariant content type (but '*' is ok) @@ -925,12 +947,15 @@ namespace Umbraco.Core.Services.Implement // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { - ((Content)content).PublishedState = PublishedState.Unpublishing; + content.PublishedState = PublishedState.Unpublishing; } else { - // if the culture we want to unpublish was already unpublished, nothing to do - if (!content.WasCulturePublished(culture)) + // If the culture we want to unpublish was already unpublished, nothing to do. + // To check for that we need to lookup the persisted content item + var persisted = content.HasIdentity ? GetById(content.Id) : null; + + if (persisted != null && !persisted.IsCulturePublished(culture)) return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); // unpublish the culture @@ -938,22 +963,35 @@ namespace Umbraco.Core.Services.Implement } // finally, "save publishing" - return SavePublishing(content, userId); + return CommitDocumentChanges(content, userId); } - /// - public PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true) + /// + /// Saves a document and publishes/unpublishes any pending publishing changes made to the document. + /// + /// + /// This is the underlying logic for both publishing and unpublishing any document + /// Pending publishing/unpublishing changes on a document are made with calls to and + /// . + /// When publishing or unpublishing a single culture, or all cultures, use + /// and . But if the flexibility to both publish and unpublish in a single operation is required + /// then this method needs to be used in combination with and + /// on the content itself - this prepares the content, but does not commit anything - and then, invoke + /// to actually commit the changes to the database. + /// The document is *always* saved, even when publishing fails. + /// + internal PublishResult CommitDocumentChanges(IContent content, int userId = 0, bool raiseEvents = true) { using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); - var result = SavePublishingInternal(scope, content, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, userId, raiseEvents); scope.Complete(); return result; } } - private PublishResult SavePublishingInternal(IScope scope, IContent content, int userId = 0, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) + private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, int userId = 0, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) { var evtMsgs = EventMessagesFactory.Get(); PublishResult publishResult = null; @@ -961,7 +999,7 @@ namespace Umbraco.Core.Services.Implement // nothing set = republish it all if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) - ((Content)content).PublishedState = PublishedState.Publishing; + content.PublishedState = PublishedState.Publishing; // state here is either Publishing or Unpublishing // (even though, Publishing to unpublish a culture may end up unpublishing everything) @@ -974,7 +1012,7 @@ namespace Umbraco.Core.Services.Implement IReadOnlyList culturesPublishing = null; IReadOnlyList culturesUnpublishing = null; IReadOnlyList culturesChanging = variesByCulture - ? content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + ? content.CultureInfos.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList() : null; var isNew = !content.HasIdentity; @@ -982,7 +1020,7 @@ namespace Umbraco.Core.Services.Implement var previouslyPublished = content.HasIdentity && content.Published; // always save - var saveEventArgs = new SaveEventArgs(content, evtMsgs); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -990,15 +1028,15 @@ namespace Umbraco.Core.Services.Implement { culturesUnpublishing = content.GetCulturesUnpublishing(); culturesPublishing = variesByCulture - ? content.PublishCultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + ? content.PublishCultureInfos.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList() : null; // ensure that the document can be published, and publish handling events, business rules, etc - publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs); + publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs); if (publishResult.Success) { // note: StrategyPublish flips the PublishedState to Publishing! - publishResult = StrategyPublish(scope, content, userId, culturesPublishing, culturesUnpublishing, evtMsgs); + publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, evtMsgs); } else { @@ -1018,7 +1056,9 @@ namespace Umbraco.Core.Services.Implement // reset published state from temp values (publishing, unpublishing) to original value // (published, unpublished) in order to save the document, unchanged - ((Content)content).Published = content.Published; + //TODO: why? this seems odd, were just setting the exact same value that it already has + // instead do we want to just set the PublishState? + content.Published = content.Published; } } @@ -1034,14 +1074,16 @@ namespace Umbraco.Core.Services.Implement // handling events, business rules, etc // note: StrategyUnpublish flips the PublishedState to Unpublishing! // note: This unpublishes the entire document (not different variants) - unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs); + unpublishResult = StrategyCanUnpublish(scope, content, evtMsgs); if (unpublishResult.Success) unpublishResult = StrategyUnpublish(scope, content, userId, evtMsgs); else { // reset published state from temp values (publishing, unpublishing) to original value // (published, unpublished) in order to save the document, unchanged - ((Content)content).Published = content.Published; + //TODO: why? this seems odd, were just setting the exact same value that it already has + // instead do we want to just set the PublishState? + content.Published = content.Published; } } else @@ -1064,8 +1106,7 @@ namespace Umbraco.Core.Services.Implement // raise the Saved event, always if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved"); } if (unpublishing) // we have tried to unpublish - won't happen in a branch @@ -1108,16 +1149,16 @@ namespace Umbraco.Core.Services.Implement if (!branchOne) // for branches, handled by SaveAndPublishBranch { scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); + scope.Events.Dispatch(Published, this, saveEventArgs.ToContentPublishedEventArgs(), nameof(Published)); } - // if was not published and now is... descendants that were 'published' (but + // it was not published and now is... descendants that were 'published' (but // had an unpublished ancestor) are 're-published' ie not explicitly published // but back as 'published' nevertheless if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id)) { var descendants = GetPublishedDescendantsLocked(content).ToArray(); - scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); + scope.Events.Dispatch(Published, this, new ContentPublishedEventArgs(descendants, false, evtMsgs), "Published"); } switch (publishResult.Result) @@ -1217,7 +1258,7 @@ namespace Umbraco.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = SavePublishing(d, d.WriterId); + result = CommitDocumentChanges(d, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1261,7 +1302,7 @@ namespace Umbraco.Core.Services.Implement if (pendingCultures.Count > 0) { - result = SavePublishing(d, d.WriterId); + result = CommitDocumentChanges(d, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); yield return result; @@ -1413,7 +1454,7 @@ namespace Umbraco.Core.Services.Implement if (!document.HasIdentity) throw new InvalidOperationException("Cannot not branch-publish a new document."); - var publishedState = ((Content)document).PublishedState; + var publishedState = document.PublishedState; if (publishedState == PublishedState.Publishing) throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch."); @@ -1467,7 +1508,9 @@ namespace Umbraco.Core.Services.Implement // trigger events for the entire branch scope.Events.Dispatch(TreeChanged, this, new TreeChange(document, TreeChangeTypes.RefreshBranch).ToEventArgs()); - scope.Events.Dispatch(Published, this, new PublishEventArgs(publishedDocuments, false, false), "Published"); + + //fixme - in the SaveAndPublishBranchOne -> CommitDocumentChangesInternal publishing/published is going to be raised there, so are we raising it 2x for the same thing? + scope.Events.Dispatch(Published, this, new ContentPublishedEventArgs(publishedDocuments, false, evtMsgs), nameof(Published)); scope.Complete(); } @@ -1495,7 +1538,7 @@ namespace Umbraco.Core.Services.Implement if (!publishCultures(document, culturesToPublish)) return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); - var result = SavePublishingInternal(scope, document, userId, branchOne: true, branchRoot: isRoot); + var result = CommitDocumentChangesInternal(scope, document, userId, branchOne: true, branchRoot: isRoot); if (result.Success) publishedDocuments.Add(document); return result; @@ -1729,7 +1772,7 @@ namespace Umbraco.Core.Services.Implement { // however, it had been masked when being trashed, so there's no need for // any special event here - just change its state - ((Content)content).PublishedState = PublishedState.Unpublishing; + content.PublishedState = PublishedState.Unpublishing; } PerformMoveLocked(content, parentId, parent, userId, moves, trashed); @@ -1906,7 +1949,7 @@ namespace Umbraco.Core.Services.Implement // a copy is not published (but not really unpublishing either) // update the create author and last edit author if (copy.Published) - ((Content)copy).Published = false; + copy.Published = false; copy.CreatorId = userId; copy.WriterId = userId; @@ -1950,7 +1993,7 @@ namespace Umbraco.Core.Services.Implement // a copy is not published (but not really unpublishing either) // update the create author and last edit author if (descendantCopy.Published) - ((Content)descendantCopy).Published = false; + descendantCopy.Published = false; descendantCopy.CreatorId = userId; descendantCopy.WriterId = userId; @@ -1997,7 +2040,7 @@ namespace Umbraco.Core.Services.Implement //track the cultures changing for auditing var culturesChanging = content.ContentType.VariesByCulture() - ? string.Join(",", content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key)) + ? string.Join(",", content.CultureInfos.Values.Where(x => x.IsDirty()).Select(x => x.Culture)) : null; // TODO: Currently there's no way to change track which variant properties have changed, we only have change @@ -2087,7 +2130,7 @@ namespace Umbraco.Core.Services.Implement private OperationResult Sort(IScope scope, IContent[] itemsA, int userId, EventMessages evtMsgs, bool raiseEvents) { - var saveEventArgs = new SaveEventArgs(itemsA); + var saveEventArgs = new ContentSavingEventArgs(itemsA, evtMsgs); if (raiseEvents) { //raise cancelable sorting event @@ -2129,15 +2172,16 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents) { + var savedEventsArgs = saveEventArgs.ToContentSavedEventArgs(); //first saved, then sorted - scope.Events.Dispatch(Saved, this, saveEventArgs, nameof(Saved)); - scope.Events.Dispatch(Sorted, this, saveEventArgs, nameof(Sorted)); + scope.Events.Dispatch(Saved, this, savedEventsArgs, nameof(Saved)); + scope.Events.Dispatch(Sorted, this, savedEventsArgs, nameof(Sorted)); } scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); if (raiseEvents && published.Any()) - scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); + scope.Events.Dispatch(Published, this, new ContentPublishedEventArgs(published, false, evtMsgs), "Published"); Audit(AuditType.Sort, userId, 0, "Sorting content performed by user"); return OperationResult.Succeed(evtMsgs); @@ -2228,12 +2272,12 @@ namespace Umbraco.Core.Services.Implement /// /// Occurs before Save /// - public static event TypedEventHandler> Saving; + public static event TypedEventHandler Saving; /// /// Occurs after Save /// - public static event TypedEventHandler> Saved; + public static event TypedEventHandler Saved; /// /// Occurs after Create @@ -2307,12 +2351,12 @@ namespace Umbraco.Core.Services.Implement /// /// Occurs before publish /// - public static event TypedEventHandler> Publishing; + public static event TypedEventHandler Publishing; /// /// Occurs after publish /// - public static event TypedEventHandler> Published; + public static event TypedEventHandler Published; /// /// Occurs before unpublish @@ -2348,14 +2392,16 @@ namespace Umbraco.Core.Services.Implement /// /// /// - /// /// + /// /// + /// + /// /// - private PublishResult StrategyCanPublish(IScope scope, IContent content, int userId, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyList culturesUnpublishing, EventMessages evtMsgs) + private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs) { // raise Publishing event - if (scope.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(content, evtMsgs))) + if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs())) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -2382,7 +2428,7 @@ namespace Umbraco.Core.Services.Implement // ensure that the document has published values // either because it is 'publishing' or because it already has a published version - if (((Content)content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) + if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document does not have published values"); return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); @@ -2438,20 +2484,20 @@ namespace Umbraco.Core.Services.Implement /// /// Publishes a document /// - /// /// - /// + /// /// + /// /// /// /// It is assumed that all publishing checks have passed before calling this method like /// - private PublishResult StrategyPublish(IScope scope, IContent content, int userId, - IReadOnlyList culturesPublishing, IReadOnlyList culturesUnpublishing, + private PublishResult StrategyPublish(IContent content, + IReadOnlyCollection culturesPublishing, IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs) { // change state to publishing - ((Content)content).PublishedState = PublishedState.Publishing; + content.PublishedState = PublishedState.Publishing; //if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result if (content.ContentType.VariesByCulture()) @@ -2486,10 +2532,9 @@ namespace Umbraco.Core.Services.Implement /// /// /// - /// /// /// - private PublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) + private PublishResult StrategyCanUnpublish(IScope scope, IContent content, EventMessages evtMsgs) { // raise Unpublishing event if (scope.Events.DispatchCancelable(Unpublishing, this, new PublishEventArgs(content, evtMsgs))) @@ -2530,7 +2575,7 @@ namespace Umbraco.Core.Services.Implement 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; + content.PublishedState = PublishedState.Unpublishing; Logger.Info("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id); return attempt; @@ -2548,7 +2593,7 @@ namespace Umbraco.Core.Services.Implement /// Deletes content items of the specified type, and only that type. Does *not* handle content types /// inheritance and compositions, which need to be managed outside of this method. /// - /// Id of the + /// Id of the /// Optional Id of the user issuing the delete operation public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = 0) { @@ -2667,7 +2712,7 @@ namespace Umbraco.Core.Services.Implement scope.ReadLock(Constants.Locks.ContentTree); var blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) - ((Content)blueprint).Blueprint = true; + blueprint.Blueprint = true; return blueprint; } } @@ -2679,7 +2724,7 @@ namespace Umbraco.Core.Services.Implement scope.ReadLock(Constants.Locks.ContentTree); var blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) - ((Content)blueprint).Blueprint = true; + blueprint.Blueprint = true; return blueprint; } } @@ -2690,7 +2735,7 @@ namespace Umbraco.Core.Services.Implement if (content.ParentId != -1) content.ParentId = -1; - ((Content)content).Blueprint = true; + content.Blueprint = true; using (var scope = ScopeProvider.CreateScope()) { @@ -2735,7 +2780,7 @@ namespace Umbraco.Core.Services.Implement content.WriterId = userId; var now = DateTime.Now; - var cultures = blueprint.CultureInfos.Any() ? blueprint.CultureInfos.Select(x=>x.Key) : ArrayOfOneNullString; + var cultures = blueprint.CultureInfos.Count > 0 ? blueprint.CultureInfos.Values.Select(x => x.Culture) : ArrayOfOneNullString; foreach (var culture in cultures) { foreach (var property in blueprint.Properties) @@ -2766,7 +2811,7 @@ namespace Umbraco.Core.Services.Implement } return _documentBlueprintRepository.Get(query).Select(x => { - ((Content)x).Blueprint = true; + x.Blueprint = true; return x; }); } @@ -2785,7 +2830,7 @@ namespace Umbraco.Core.Services.Implement var blueprints = _documentBlueprintRepository.Get(query).Select(x => { - ((Content)x).Blueprint = true; + x.Blueprint = true; return x; }).ToArray(); diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs index a0b952a75a..f620d31fe9 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs @@ -61,7 +61,7 @@ namespace Umbraco.Core.Services.Implement var filename = Path.GetFileNameWithoutExtension(localCopy.FullName).Replace("_", "-"); // TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct - // names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we + // names instead of storing them as 2 letters but actually having a 4 letter culture. So now, we // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea) // that any 4 letter file is named with the actual culture that it is! diff --git a/src/Umbraco.Core/Services/Implement/NotificationService.cs b/src/Umbraco.Core/Services/Implement/NotificationService.cs index d981809364..2b21945ba8 100644 --- a/src/Umbraco.Core/Services/Implement/NotificationService.cs +++ b/src/Umbraco.Core/Services/Implement/NotificationService.cs @@ -345,8 +345,8 @@ namespace Umbraco.Core.Services.Implement { //Create the HTML based summary (ul of culture names) - var culturesChanged = content.CultureInfos.Where(x => x.Value.WasDirty()) - .Select(x => x.Key) + var culturesChanged = content.CultureInfos.Values.Where(x => x.WasDirty()) + .Select(x => x.Culture) .Select(_localizationService.GetLanguageByIsoCode) .WhereNotNull() .Select(x => x.CultureName); @@ -363,8 +363,8 @@ namespace Umbraco.Core.Services.Implement { //Create the text based summary (csv of culture names) - var culturesChanged = string.Join(", ", content.CultureInfos.Where(x => x.Value.WasDirty()) - .Select(x => x.Key) + var culturesChanged = string.Join(", ", content.CultureInfos.Values.Where(x => x.WasDirty()) + .Select(x => x.Culture) .Select(_localizationService.GetLanguageByIsoCode) .WhereNotNull() .Select(x => x.CultureName)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index eda02148fe..20eb66792c 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -302,6 +302,11 @@ + + + + + @@ -394,6 +399,7 @@ + diff --git a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs index 8ea13fc920..07699f77ed 100644 --- a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs +++ b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs @@ -262,6 +262,40 @@ namespace Umbraco.Tests.Composing AssertCollection(col, typeof(Resolved1), typeof(Resolved3), typeof(Resolved2)); } + [Test] + public void CanInsertIntoBuilderAfter() + { + var builder = _composition.WithCollectionBuilder() + .Append() + .Append() + .InsertAfter(); + + Assert.IsTrue(builder.Has()); + Assert.IsTrue(builder.Has()); + Assert.IsTrue(builder.Has()); + + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); + AssertCollection(col, typeof(Resolved1), typeof(Resolved3), typeof(Resolved2)); + } + + [Test] + public void CanInsertIntoBuilderAfterLast() + { + var builder = _composition.WithCollectionBuilder() + .Append() + .Append() + .InsertAfter(); + + Assert.IsTrue(builder.Has()); + Assert.IsTrue(builder.Has()); + Assert.IsTrue(builder.Has()); + + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); + AssertCollection(col, typeof(Resolved1), typeof(Resolved2), typeof(Resolved3)); + } + [Test] public void CannotInsertIntoBuilderBeforeOnceCollectionIsCreated() { @@ -314,7 +348,7 @@ namespace Umbraco.Tests.Composing var factory = _composition.CreateFactory(); using (factory.BeginScope()) - { + { var col1 = factory.GetInstance(); AssertCollection(col1, typeof(Resolved1), typeof(Resolved2)); diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs index c62614ef22..17a18781b4 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs @@ -587,7 +587,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache { int id; if (int.TryParse(itemm.GetAttribute("id", ""), out id) == false) - continue; // wtf? + continue; // uh? var captured = itemm; var cacheValues = GetCacheValues(id, idd => ConvertFromXPathNavigator(captured)); mediaList.Add(CreateFromCacheValues(cacheValues)); diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index ad49fb226b..7fc1bd3ce4 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -460,16 +460,16 @@ namespace Umbraco.Tests.Models Assert.IsTrue(content.WasPropertyDirty("CultureInfos")); foreach(var culture in content.CultureInfos) { - Assert.IsTrue(culture.Value.WasDirty()); - Assert.IsTrue(culture.Value.WasPropertyDirty("Name")); - Assert.IsTrue(culture.Value.WasPropertyDirty("Date")); + Assert.IsTrue(culture.WasDirty()); + Assert.IsTrue(culture.WasPropertyDirty("Name")); + Assert.IsTrue(culture.WasPropertyDirty("Date")); } Assert.IsTrue(content.WasPropertyDirty("PublishCultureInfos")); foreach (var culture in content.PublishCultureInfos) { - Assert.IsTrue(culture.Value.WasDirty()); - Assert.IsTrue(culture.Value.WasPropertyDirty("Name")); - Assert.IsTrue(culture.Value.WasPropertyDirty("Date")); + Assert.IsTrue(culture.WasDirty()); + Assert.IsTrue(culture.WasPropertyDirty("Name")); + Assert.IsTrue(culture.WasPropertyDirty("Date")); } } @@ -752,22 +752,24 @@ namespace Umbraco.Tests.Models } [Test] + [Ignore("Need to reimplement this logic for v8")] public void Can_Change_ContentType_On_Content_And_Clear_Old_PropertyTypes() { - // Arrange - var contentType = MockedContentTypes.CreateTextPageContentType(); - var simpleContentType = MockedContentTypes.CreateSimpleContentType(); - Mock.Get(_contentTypeService).As().Setup(x => x.Get(It.IsAny())).Returns(contentType); + throw new NotImplementedException(); + //Mock.Get(_contentTypeService).As().Setup(x => x.Get(It.IsAny())).Returns(contentType); - var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + //// Arrange + //var contentType = MockedContentTypes.CreateTextPageContentType(); + //var simpleContentType = MockedContentTypes.CreateSimpleContentType(); + //var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); - // Act - content.ChangeContentType(simpleContentType, true); + //// Act + //content.ChangeContentType(simpleContentType, true); - // Assert - Assert.That(content.Properties.Contains("author"), Is.True); - Assert.That(content.Properties.Contains("keywords"), Is.False); - Assert.That(content.Properties.Contains("description"), Is.False); + //// Assert + //Assert.That(content.Properties.Contains("author"), Is.True); + //Assert.That(content.Properties.Contains("keywords"), Is.False); + //Assert.That(content.Properties.Contains("description"), Is.False); } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 2d45c24d0f..03aae74920 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -311,7 +311,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(4, contentType.PropertyTypes.Count()); - // remove all templates - since they are not saved, they would break the (wtf) mapping code + // remove all templates - since they are not saved, they would break the (!) mapping code contentType.AllowedTemplates = new ITemplate[0]; // there is NO mapping from display to contentType, but only from save diff --git a/src/Umbraco.Tests/Services/AmbiguousEventTests.cs b/src/Umbraco.Tests/Services/AmbiguousEventTests.cs new file mode 100644 index 0000000000..e137da1188 --- /dev/null +++ b/src/Umbraco.Tests/Services/AmbiguousEventTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Reflection; +using System.Text; +using NUnit.Framework; +using Umbraco.Core.Events; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + public class AmbiguousEventTests + { + [Explicit] + [TestCase(typeof(ContentService))] + [TestCase(typeof(MediaService))] + public void ListAmbiguousEvents(Type serviceType) + { + var typedEventHandler = typeof(TypedEventHandler<,>); + + // get all events + var events = serviceType.GetEvents(BindingFlags.Static | BindingFlags.Public); + + string TypeName(Type type) + { + if (!type.IsGenericType) + return type.Name; + var sb = new StringBuilder(); + TypeNameSb(type, sb); + return sb.ToString(); + } + + void TypeNameSb(Type type, StringBuilder sb) + { + var name = type.Name; + var pos = name.IndexOf('`'); + name = pos > 0 ? name.Substring(0, pos) : name; + sb.Append(name); + if (!type.IsGenericType) + return; + sb.Append("<"); + var first = true; + foreach (var arg in type.GetGenericArguments()) + { + if (first) first = false; + else sb.Append(", "); + TypeNameSb(arg, sb); + } + sb.Append(">"); + } + + foreach (var e in events) + { + // only continue if this is a TypedEventHandler + if (!e.EventHandlerType.IsGenericType) continue; + var typeDef = e.EventHandlerType.GetGenericTypeDefinition(); + if (typedEventHandler != typeDef) continue; + + // get the event args type + var eventArgsType = e.EventHandlerType.GenericTypeArguments[1]; + + // try to find the event back, based upon sender type + args type + // exclude -ing (eg Saving) events, we don't deal with them in EventDefinitionBase (they always trigger) + var found = EventNameExtractor.FindEvents(serviceType, eventArgsType, EventNameExtractor.MatchIngNames); + + if (found.Length == 1) continue; + + if (found.Length == 0) + { + Console.WriteLine($"{typeof(ContentService).Name} {e.Name} {TypeName(eventArgsType)} NotFound"); + continue; + } + + Console.WriteLine($"{typeof(ContentService).Name} {e.Name} {TypeName(eventArgsType)} Ambiguous"); + Console.WriteLine("\t" + string.Join(", ", found)); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/ContentServiceEventTests.cs b/src/Umbraco.Tests/Services/ContentServiceEventTests.cs new file mode 100644 index 0000000000..1547ec232a --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentServiceEventTests.cs @@ -0,0 +1,219 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true, + Logger = UmbracoTestOptions.Logger.Console)] + public class ContentServiceEventTests : TestWithSomeContentBase + { + public override void SetUp() + { + base.SetUp(); + ContentRepositoryBase.ThrowOnWarning = true; + } + + public override void TearDown() + { + ContentRepositoryBase.ThrowOnWarning = false; + base.TearDown(); + } + + [Test] + public void SavingTest() + { + var languageService = ServiceContext.LocalizationService; + + languageService.Save(new Language("fr-FR")); + + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextPageContentType(); + ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); + contentType.Variations = ContentVariation.Culture; + foreach (var propertyType in contentType.PropertyTypes) + propertyType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + var contentService = ServiceContext.ContentService; + + IContent document = new Content("content", -1, contentType); + document.SetCultureName("hello", "en-US"); + document.SetCultureName("bonjour", "fr-FR"); + contentService.Save(document); + + //re-get - dirty properties need resetting + document = contentService.GetById(document.Id); + + // properties: title, bodyText, keywords, description + document.SetValue("title", "title-en", "en-US"); + + void OnSaving(IContentService sender, ContentSavingEventArgs e) + { + var saved = e.SavedEntities.First(); + + Assert.AreSame(document, saved); + + Assert.IsTrue(e.IsSavingCulture(saved, "en-US")); + Assert.IsFalse(e.IsSavingCulture(saved, "fr-FR")); + } + + void OnSaved(IContentService sender, ContentSavedEventArgs e) + { + var saved = e.SavedEntities.First(); + + Assert.AreSame(document, saved); + + Assert.IsTrue(e.HasSavedCulture(saved, "en-US")); + Assert.IsFalse(e.HasSavedCulture(saved, "fr-FR")); + } + + ContentService.Saving += OnSaving; + ContentService.Saved += OnSaved; + contentService.Save(document); + ContentService.Saving -= OnSaving; + ContentService.Saved -= OnSaved; + } + + [Test] + public void PublishingTest() + { + var languageService = ServiceContext.LocalizationService; + + languageService.Save(new Language("fr-FR")); + + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextPageContentType(); + ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); + contentType.Variations = ContentVariation.Culture; + foreach (var propertyType in contentType.PropertyTypes) + propertyType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + var contentService = ServiceContext.ContentService; + + IContent document = new Content("content", -1, contentType); + document.SetCultureName("hello", "en-US"); + document.SetCultureName("bonjour", "fr-FR"); + contentService.Save(document); + + Assert.IsFalse(document.IsCulturePublished("fr-FR")); + Assert.IsFalse(document.IsCulturePublished("en-US")); + + //re-get - dirty properties need resetting + document = contentService.GetById(document.Id); + + void OnPublishing(IContentService sender, ContentPublishingEventArgs e) + { + var publishing = e.PublishedEntities.First(); + + Assert.AreSame(document, publishing); + + Assert.IsFalse(e.IsPublishingCulture(publishing, "en-US")); + Assert.IsTrue(e.IsPublishingCulture(publishing, "fr-FR")); + } + + void OnPublished(IContentService sender, ContentPublishedEventArgs e) + { + var published = e.PublishedEntities.First(); + + Assert.AreSame(document, published); + + Assert.IsFalse(e.HasPublishedCulture(published, "en-US")); + Assert.IsTrue(e.HasPublishedCulture(published, "fr-FR")); + } + + ContentService.Publishing += OnPublishing; + ContentService.Published += OnPublished; + contentService.SaveAndPublish(document, "fr-FR"); + ContentService.Publishing -= OnPublishing; + ContentService.Published -= OnPublished; + + document = contentService.GetById(document.Id); + + // ensure it works and does not throw + Assert.IsTrue(document.IsCulturePublished("fr-FR")); + Assert.IsFalse(document.IsCulturePublished("en-US")); + } + + [Test] + public void UnpublishingTest() + { + var languageService = ServiceContext.LocalizationService; + + languageService.Save(new Language("fr-FR")); + + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextPageContentType(); + ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); + contentType.Variations = ContentVariation.Culture; + foreach (var propertyType in contentType.PropertyTypes) + propertyType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + var contentService = (ContentService)ServiceContext.ContentService; + + IContent document = new Content("content", -1, contentType); + document.SetCultureName("hello", "en-US"); + document.SetCultureName("bonjour", "fr-FR"); + contentService.SaveAndPublish(document); + + Assert.IsTrue(document.IsCulturePublished("fr-FR")); + Assert.IsTrue(document.IsCulturePublished("en-US")); + + //re-get - dirty properties need resetting + document = contentService.GetById(document.Id); + + void OnPublishing(IContentService sender, ContentPublishingEventArgs e) + { + var publishing = e.PublishedEntities.First(); + + Assert.AreSame(document, publishing); + + Assert.IsFalse(e.IsPublishingCulture(publishing, "en-US")); + Assert.IsFalse(e.IsPublishingCulture(publishing, "fr-FR")); + + Assert.IsFalse(e.IsUnpublishingCulture(publishing, "en-US")); + Assert.IsTrue(e.IsUnpublishingCulture(publishing, "fr-FR")); + } + + void OnPublished(IContentService sender, ContentPublishedEventArgs e) + { + var published = e.PublishedEntities.First(); + + Assert.AreSame(document, published); + + Assert.IsFalse(e.HasPublishedCulture(published, "en-US")); + Assert.IsFalse(e.HasPublishedCulture(published, "fr-FR")); + + Assert.IsFalse(e.HasUnpublishedCulture(published, "en-US")); + Assert.IsTrue(e.HasUnpublishedCulture(published, "fr-FR")); + } + + document.UnpublishCulture("fr-FR"); + + ContentService.Publishing += OnPublishing; + ContentService.Published += OnPublished; + contentService.CommitDocumentChanges(document); + ContentService.Publishing -= OnPublishing; + ContentService.Published -= OnPublished; + + document = contentService.GetById(document.Id); + + Assert.IsFalse(document.IsCulturePublished("fr-FR")); + Assert.IsTrue(document.IsCulturePublished("en-US")); + } + } +} diff --git a/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs b/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs index 4afd5e33eb..d2343d3dea 100644 --- a/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs +++ b/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs @@ -264,9 +264,7 @@ namespace Umbraco.Tests.Services vRoot.SetValue("vp", "changed.es", "es"); ServiceContext.ContentService.Save(vRoot); // now root has drafts in all cultures - iv1.PublishCulture("de"); - iv1.PublishCulture("ru"); - ServiceContext.ContentService.SavePublishing(iv1); // now iv1 de and ru are published + ServiceContext.ContentService.SaveAndPublish(iv1, new []{"de", "ru"}); // now iv1 de and ru are published iv1.SetValue("ip", "changed"); iv1.SetValue("vp", "changed.de", "de"); @@ -345,10 +343,8 @@ namespace Umbraco.Tests.Services iv11.SetValue("vp", "iv11.es", "es"); ServiceContext.ContentService.Save(iv11); - iv11.PublishCulture("de"); iv11.SetCultureName("iv11.ru", "ru"); - iv11.PublishCulture("ru"); - ServiceContext.ContentService.SavePublishing(iv11); + ServiceContext.ContentService.SaveAndPublish(iv11, new []{"de", "ru"}); Assert.AreEqual("iv11.de", iv11.GetValue("vp", "de", published: true)); Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true)); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 017f1f50ec..94cbf69b63 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -6,7 +6,6 @@ using System.Threading; using Moq; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -14,14 +13,11 @@ using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Core.Events; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Implement; using Umbraco.Tests.Testing; -using System.Reflection; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; namespace Umbraco.Tests.Services { @@ -60,32 +56,6 @@ namespace Umbraco.Tests.Services Composition.RegisterUnique(factory => Mock.Of()); } - /// - /// Used to list out all ambiguous events that will require dispatching with a name - /// - [Test, Explicit] - public void List_Ambiguous_Events() - { - var events = ServiceContext.ContentService.GetType().GetEvents(BindingFlags.Static | BindingFlags.Public); - var typedEventHandler = typeof(TypedEventHandler<,>); - foreach(var e in events) - { - //only continue if this is a TypedEventHandler - if (!e.EventHandlerType.IsGenericType) continue; - var typeDef = e.EventHandlerType.GetGenericTypeDefinition(); - if (typedEventHandler != typeDef) continue; - - //get the event arg type - var eventArgType = e.EventHandlerType.GenericTypeArguments[1]; - - var found = EventNameExtractor.FindEvent(typeof(ContentService), eventArgType, EventNameExtractor.MatchIngNames); - if (!found.Success && found.Result.Error == EventNameExtractorError.Ambiguous) - { - Console.WriteLine($"Ambiguous event, source: {typeof(ContentService)}, args: {eventArgType}"); - } - } - } - [Test] public void Create_Blueprint() { @@ -763,36 +733,29 @@ namespace Umbraco.Tests.Services content.PublishCulture(langFr.IsoCode); content.PublishCulture(langUk.IsoCode); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsFalse(content.WasCulturePublished(langFr.IsoCode)); //not persisted yet, will be false Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - Assert.IsFalse(content.WasCulturePublished(langUk.IsoCode)); //not persisted yet, will be false - var published = ServiceContext.ContentService.SavePublishing(content); + var published = ServiceContext.ContentService.SaveAndPublish(content, new[]{ langFr.IsoCode , langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + //re-get content = ServiceContext.ContentService.GetById(content.Id); Assert.IsTrue(published.Success); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.WasCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - Assert.IsTrue(content.WasCulturePublished(langUk.IsoCode)); var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); Assert.IsTrue(unpublished.Success); Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - //this is slightly confusing but this will be false because this method is used for checking the state of the current model, - //but the state on the model has changed with the above Unpublish call - Assert.IsFalse(content.WasCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); //re-get content = ServiceContext.ContentService.GetById(content.Id); Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - //this is slightly confusing but this will be false because this method is used for checking the state of a current model, - //but we've re-fetched from the database - Assert.IsFalse(content.WasCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - Assert.IsTrue(content.WasCulturePublished(langUk.IsoCode)); + } @@ -819,10 +782,9 @@ namespace Umbraco.Tests.Services IContent content = new Content("content", -1, contentType); content.SetCultureName("content-en", langGB.IsoCode); content.SetCultureName("content-fr", langFr.IsoCode); - content.PublishCulture(langGB.IsoCode); - content.PublishCulture(langFr.IsoCode); - Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content).Success); - + + Assert.IsTrue(ServiceContext.ContentService.SaveAndPublish(content, new []{ langGB.IsoCode , langFr.IsoCode }).Success); + //re-get content = ServiceContext.ContentService.GetById(content.Id); Assert.AreEqual(PublishedState.Published, content.PublishedState); @@ -861,8 +823,7 @@ namespace Umbraco.Tests.Services IContent content = new Content("content", -1, contentType); content.SetCultureName("content-fr", langFr.IsoCode); - content.PublishCulture(langFr.IsoCode); - var published = ServiceContext.ContentService.SavePublishing(content); + var published = ServiceContext.ContentService.SaveAndPublish(content, langFr.IsoCode); //audit log will only show that french was published var lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); Assert.AreEqual($"Published languages: French (France)", lastLog.Comment); @@ -870,8 +831,7 @@ namespace Umbraco.Tests.Services //re-get content = ServiceContext.ContentService.GetById(content.Id); content.SetCultureName("content-en", langGB.IsoCode); - content.PublishCulture(langGB.IsoCode); - published = ServiceContext.ContentService.SavePublishing(content); + published = ServiceContext.ContentService.SaveAndPublish(content, langGB.IsoCode); //audit log will only show that english was published lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); Assert.AreEqual($"Published languages: English (United Kingdom)", lastLog.Comment); @@ -895,15 +855,12 @@ namespace Umbraco.Tests.Services IContent content = new Content("content", -1, contentType); content.SetCultureName("content-fr", langFr.IsoCode); content.SetCultureName("content-gb", langGB.IsoCode); - content.PublishCulture(langGB.IsoCode); - content.PublishCulture(langFr.IsoCode); - var published = ServiceContext.ContentService.SavePublishing(content); + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] {langGB.IsoCode, langFr.IsoCode}); Assert.IsTrue(published.Success); //re-get content = ServiceContext.ContentService.GetById(content.Id); - content.UnpublishCulture(langFr.IsoCode); //unpublish non-mandatory lang - var unpublished = ServiceContext.ContentService.SavePublishing(content); + var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); //audit log will only show that french was unpublished var lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); Assert.AreEqual($"Unpublished languages: French (France)", lastLog.Comment); @@ -911,8 +868,7 @@ namespace Umbraco.Tests.Services //re-get content = ServiceContext.ContentService.GetById(content.Id); content.SetCultureName("content-en", langGB.IsoCode); - content.UnpublishCulture(langGB.IsoCode); //unpublish mandatory lang - unpublished = ServiceContext.ContentService.SavePublishing(content); + unpublished = ServiceContext.ContentService.Unpublish(content, langGB.IsoCode); //audit log will only show that english was published var logs = ServiceContext.AuditService.GetLogs(content.Id).ToList(); Assert.AreEqual($"Unpublished languages: English (United Kingdom)", logs[logs.Count - 2].Comment); @@ -927,8 +883,7 @@ namespace Umbraco.Tests.Services var content = contentService.GetById(NodeDto.NodeIdSeed + 2); // Act - content.PublishCulture(); - var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var published = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.True); @@ -982,8 +937,7 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Home", content.Name); content.Name = "foo"; - content.PublishCulture(); - var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var published = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); Assert.That(published.Success, Is.True); Assert.That(content.Published, Is.True); @@ -1030,10 +984,7 @@ namespace Umbraco.Tests.Services Assert.IsTrue(parentPublished.Success); Assert.IsTrue(parent.Published); - var contentCanPublishValues = content.PublishCulture(); - // content cannot publish values because they are invalid - Assert.IsFalse(contentCanPublishValues); Assert.IsNotEmpty(content.ValidateProperties()); // and therefore cannot be published, @@ -1047,6 +998,8 @@ namespace Umbraco.Tests.Services [Test] public void Can_Publish_And_Unpublish_Cultures_In_Single_Operation() { + //TODO: This is using an internal API - we aren't exposing this publicly (at least for now) but we'll keep the test around + var langFr = new Language("fr"); var langDa = new Language("da"); ServiceContext.LocalizationService.Save(langFr); @@ -1061,7 +1014,7 @@ namespace Umbraco.Tests.Services content.SetCultureName("name-da", langDa.IsoCode); content.PublishCulture(langFr.IsoCode); - var result = ServiceContext.ContentService.SavePublishing(content); + var result = ((ContentService)ServiceContext.ContentService).CommitDocumentChanges(content); Assert.IsTrue(result.Success); content = ServiceContext.ContentService.GetById(content.Id); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); @@ -1070,7 +1023,7 @@ namespace Umbraco.Tests.Services content.UnpublishCulture(langFr.IsoCode); content.PublishCulture(langDa.IsoCode); - result = ServiceContext.ContentService.SavePublishing(content); + result = ((ContentService)ServiceContext.ContentService).CommitDocumentChanges(content); Assert.IsTrue(result.Success); Assert.AreEqual(PublishResultType.SuccessMixedCulture, result.Result); @@ -1149,12 +1102,10 @@ namespace Umbraco.Tests.Services contentService.Save(content); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); - parent.PublishCulture(); - var parentPublished = contentService.SavePublishing(parent, Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + var parentPublished = contentService.SaveAndPublish(parent, userId: Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act - content.PublishCulture(); - var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var published = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); @@ -1192,12 +1143,10 @@ namespace Umbraco.Tests.Services contentService.Save(content, Constants.Security.SuperUserId); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); - parent.PublishCulture(); - var parentPublished = contentService.SavePublishing(parent, Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + var parentPublished = contentService.SaveAndPublish(parent, userId: Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act - content.PublishCulture(); - var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var published = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); @@ -1249,8 +1198,7 @@ namespace Umbraco.Tests.Services var content = contentService.GetById(NodeDto.NodeIdSeed + 5); // Act - content.PublishCulture(); - var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var published = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.False); @@ -1267,8 +1215,7 @@ namespace Umbraco.Tests.Services content.SetValue("author", "Barack Obama"); // Act - content.PublishCulture(); - var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var published = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1292,15 +1239,13 @@ namespace Umbraco.Tests.Services content.SetValue("author", "Barack Obama"); // Act - content.PublishCulture(); - var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var published = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); var childContent = contentService.Create("Child", content.Id, "umbTextpage", Constants.Security.SuperUserId); // Reset all identity properties childContent.Id = 0; childContent.Path = null; ((Content)childContent).ResetIdentity(); - childContent.PublishCulture(); - var childPublished = contentService.SavePublishing(childContent, Constants.Security.SuperUserId); + var childPublished = contentService.SaveAndPublish(childContent, userId: Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1639,14 +1584,12 @@ namespace Umbraco.Tests.Services content1.PropertyValues(obj); content1.ResetDirtyProperties(false); ServiceContext.ContentService.Save(content1, Constants.Security.SuperUserId); - content1.PublishCulture(); - Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content1, 0).Success); + Assert.IsTrue(ServiceContext.ContentService.SaveAndPublish(content1, userId: 0).Success); var content2 = MockedContent.CreateBasicContent(contentType); content2.PropertyValues(obj); content2.ResetDirtyProperties(false); ServiceContext.ContentService.Save(content2, Constants.Security.SuperUserId); - content2.PublishCulture(); - Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content2, 0).Success); + Assert.IsTrue(ServiceContext.ContentService.SaveAndPublish(content2, userId: 0).Success); var editorGroup = ServiceContext.UserService.GetUserGroupByAlias("editor"); editorGroup.StartContentId = content1.Id; @@ -2721,9 +2664,7 @@ namespace Umbraco.Tests.Services // act - content.PublishCulture(langFr.IsoCode); - content.PublishCulture(langUk.IsoCode); - contentService.SavePublishing(content); + contentService.SaveAndPublish(content, new[]{ langFr.IsoCode, langUk.IsoCode }); // both FR and UK have been published, // and content has been published, @@ -2827,8 +2768,7 @@ namespace Umbraco.Tests.Services // act // cannot just 'save' since we are changing what's published! - content.UnpublishCulture(langFr.IsoCode); - contentService.SavePublishing(content); + contentService.Unpublish(content, langFr.IsoCode); // content has been published, // the french culture is gone @@ -2918,8 +2858,10 @@ namespace Umbraco.Tests.Services // act // that HAS to be SavePublishing, because SaveAndPublish would just republish everything! - - contentService.SavePublishing(content); + //TODO: This is using an internal API - the test can't pass without this but we want to keep the test here + // will need stephane to have a look at this test at some stage since there is a lot of logic here that we + // want to keep on testing but don't need the public API to do these more complicated things. + ((ContentService)contentService).CommitDocumentChanges(content); // content has been re-published, // everything is back to what it was before being unpublished @@ -2959,8 +2901,7 @@ namespace Umbraco.Tests.Services // act - content.PublishCulture(langUk.IsoCode); - contentService.SavePublishing(content); + contentService.SaveAndPublish(content, langUk.IsoCode); content2 = contentService.GetById(content.Id); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index db3aa10d44..110accdecf 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -153,7 +153,7 @@ namespace Umbraco.Tests.TestHelpers var localizationService = GetLazyService(factory, c => new LocalizationService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c))); var userService = GetLazyService(factory, c => new UserService(scopeProvider, logger, eventMessagesFactory, runtimeState, GetRepo(c), GetRepo(c),globalSettings)); var dataTypeService = GetLazyService(factory, c => new DataTypeService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); - var contentService = GetLazyService(factory, c => new ContentService(scopeProvider, logger, eventMessagesFactory, mediaFileSystem, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); + var contentService = GetLazyService(factory, c => new ContentService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); var notificationService = GetLazyService(factory, c => new NotificationService(scopeProvider, userService.Value, contentService.Value, localizationService.Value, logger, GetRepo(c), globalSettings, umbracoSettings.Content)); var serverRegistrationService = GetLazyService(factory, c => new ServerRegistrationService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var memberGroupService = GetLazyService(factory, c => new MemberGroupService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 702a99b510..c404e0404d 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -157,7 +157,7 @@ namespace Umbraco.Tests.Testing // etc ComposeWeb(); - ComposeWtf(); + ComposeMisc(); // not sure really Compose(Composition); @@ -233,7 +233,7 @@ namespace Umbraco.Tests.Testing } - protected virtual void ComposeWtf() + protected virtual void ComposeMisc() { // what else? var runtimeStateMock = new Mock(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 41c9581131..3bfc3387fe 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -141,6 +141,8 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 235918735f..4104a663d3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -7,161 +7,85 @@ var evts = []; var allowedNumberOfVisibleEditors = 3; - var editorIndent = 60; - + scope.editors = []; - + function addEditor(editor) { + editor.inFront = true; + editor.moveRight = true; + editor.level = 0; + editor.styleIndex = 0; - if (!editor.style) - editor.style = {}; - - editor.animating = true; - - showOverlayOnPrevEditor(); - - var i = allowedNumberOfVisibleEditors; - var len = scope.editors.length; - while(i= allowedNumberOfVisibleEditors) { - animeConfig.left = i * editorIndent; - } else { - animeConfig.left = (i + 1) * editorIndent; - } - - anime(animeConfig); - - i++; - } - + editor.infinityMode = true; // push the new editor to the dom scope.editors.push(editor); - - - var indentValue = scope.editors.length * editorIndent; - - // don't allow indent larger than what - // fits the max number of visible editors - if(scope.editors.length >= allowedNumberOfVisibleEditors) { - indentValue = allowedNumberOfVisibleEditors * editorIndent; - } - - // indent all large editors - if(editor.size !== "small") { - editor.style.left = indentValue + "px"; - } - - editor.style._tx = 100; - editor.style.transform = "translateX("+editor.style._tx+"%)"; - - // animation config - anime({ - targets: editor.style, - _tx: [100, 0], - easing: 'easeOutExpo', - duration: 480, - update: () => { - editor.style.transform = "translateX("+editor.style._tx+"%)"; - scope.$digest(); - }, - complete: function() { - editor.animating = false; - scope.$digest(); - } - }); - - - } - - function removeEditor(editor) { + $timeout(() => { + editor.moveRight = false; + }) editor.animating = true; + setTimeout(revealEditorContent.bind(this, editor), 400); - editor.style._tx = 0; - editor.style.transform = "translateX("+editor.style._tx+"%)"; + updateEditors(); + + } + + function removeEditor(editor) { - // animation config - anime({ - targets: editor.style, - _tx: [0, 100], - easing: 'easeInExpo', - duration: 360, - update: () => { - editor.style.transform = "translateX("+editor.style._tx+"%)"; - scope.$digest(); - }, - complete: function() { - scope.editors.splice(-1,1); - removeOverlayFromPrevEditor(); - scope.$digest(); - } - }); + editor.moveRight = true; + editor.animating = true; + setTimeout(removeEditorFromDOM.bind(this, editor), 400); - expandEditors(); - + updateEditors(-1); } - - function expandEditors() { + + function revealEditorContent(editor) { - var i = allowedNumberOfVisibleEditors + 1; - var len = scope.editors.length-1; + editor.animating = false; + + scope.$digest(); + + } + + function removeEditorFromDOM(editor) { + + // push the new editor to the dom + var index = scope.editors.indexOf(editor); + if (index !== -1) { + scope.editors.splice(index, 1); + } + + updateEditors(); + + scope.$digest(); + + } + + /** update layer positions. With ability to offset positions, needed for when an item is moving out, then we dont want it to influence positions */ + function updateEditors(offset) { + + offset = offset || 0;// fallback value. + + var len = scope.editors.length; + var calcLen = len + offset; + var ceiling = Math.min(calcLen, allowedNumberOfVisibleEditors); + var origin = Math.max(calcLen-1, 0)-ceiling; + var i = 0; while(i= ceiling; i++; } - - - } - // show backdrop on previous editor - function showOverlayOnPrevEditor() { - var numberOfEditors = scope.editors.length; - if(numberOfEditors > 0) { - scope.editors[numberOfEditors - 1].showOverlay = true; - } } - - function removeOverlayFromPrevEditor() { - var numberOfEditors = scope.editors.length; - if(numberOfEditors > 0) { - scope.editors[numberOfEditors - 1].showOverlay = false; - } - } - + evts.push(eventsService.on("appState.editors.open", function (name, args) { addEditor(args.editor); })); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js index aac11bfd22..9401cacab1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js @@ -36,7 +36,7 @@ Use this directive to generate color swatches to pick from. scope.setColor = function (color, $index, $event) { scope.selectedColor = color; if (scope.onSelect) { - scope.onSelect(color.color, $index, $event); + scope.onSelect({color: color, $index: $index, $event: $event}); $event.stopPropagation(); } }; @@ -55,7 +55,7 @@ Use this directive to generate color swatches to pick from. colors: '=?', size: '@', selectedColor: '=', - onSelect: '=', + onSelect: '&', useLabel: '=', useColorClass: '=?' }, diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less index 6dd77c56b1..f1fa8245ea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less @@ -4,6 +4,7 @@ right: 0; bottom: 0; left: 0; + overflow: hidden; } .umb-editor { @@ -17,7 +18,49 @@ } .umb-editor--animating { - will-change: transform, width, left; + //will-change: transform, width, left; +} +.umb-editor--infinityMode { + transform: none; + will-change: transform; + transition: transform 400ms ease-in-out; + &.moveRight { + transform: translateX(110%); + } +} + +.umb-editor--outOfRange { + //left:0; + transform: none; + display: none; + will-change: auto; + transition: display 0s 320ms; +} +.umb-editor--level0 { + //left:0; + transform: none; +} +.umb-editor--level1 { + //left:60px; + transform: translateX(60px); +} +.umb-editor--level2 { + //left:120px; + transform: translateX(120px); +} +.umb-editor--level3 { + //left:180px; + transform: translateX(180px); +} + +.umb-editor--n1 { + right:60px; +} +.umb-editor--n2 { + right:120px; +} +.umb-editor--n3 { + right:180px; } // hide all infinite editors by default @@ -28,20 +71,14 @@ .umb-editor--small { width: 500px; + will-change: transform; left: auto; - + .umb-editor-container { max-width: 500px; } } -@keyframes umb-editor__overlay_fade_opacity { - from { - opacity:0; - } - to { - opacity:1; - } -} + .umb-editor__overlay { position: absolute; top: 0; @@ -50,6 +87,14 @@ left: 0; background: rgba(0,0,0,0.4); z-index: @zIndexEditor; - - animation:umb-editor__overlay_fade_opacity 600ms; + visibility: hidden; + opacity: 0; + transition: opacity 320ms 20ms, visibility 0s 400ms; +} + +#contentcolumn > .umb-editor__overlay, +.--notInFront .umb-editor__overlay { + visibility: visible; + opacity: 1; + transition: opacity 320ms 20ms, visibility 0s; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js index 16d6cf23fc..471d23ae84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js @@ -49,7 +49,7 @@ function IconPickerController($scope, iconHelper, localizationService) { }); // set a default color if nothing is passed in - vm.color = $scope.model.color ? $scope.model.color : vm.colors[0].value; + vm.color = $scope.model.color ? findColor($scope.model.color) : vm.colors[0]; // if an icon is passed in - preselect it vm.icon = $scope.model.icon ? $scope.model.icon : undefined; @@ -70,8 +70,13 @@ function IconPickerController($scope, iconHelper, localizationService) { submit(); } + function findColor(value) { + return _.findWhere(vm.colors, {value: value}); + } + function selectColor(color, $index, $event) { - $scope.model.color = color; + $scope.model.color = color.value; + vm.color = color; } function close() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 55c4317279..3caa6ae03d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -37,16 +37,16 @@ colors="vm.colors" selected-color="vm.color" size="s" - on-select="vm.selectColor"> + on-select="vm.selectColor(color)">
-
    +
    • - +
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 275c814761..829582329f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -1,7 +1,8 @@
      -
/// - /// /// /// /// if the content is variant this will return an array of cultures that will be published (passed validation rules) @@ -1200,10 +1199,12 @@ namespace Umbraco.Web.Editors if (canPublish) { + var culturesToPublish = cultureVariants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); + //proceed to publish if all validation still succeeds - var publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id); + var publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, culturesToPublish, Security.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; - successfulCultures = contentItem.Variants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); + successfulCultures = culturesToPublish; return publishStatus; } else @@ -1222,6 +1223,10 @@ namespace Umbraco.Web.Editors /// /// /// + /// + /// + /// + /// /// private bool ValidatePublishingMandatoryLanguages( ContentItemSave contentItem, @@ -1243,7 +1248,7 @@ namespace Umbraco.Web.Editors var isPublished = contentItem.PersistedContent.Published && contentItem.PersistedContent.IsCulturePublished(culture); result.Add((mandatoryVariant, isPublished)); - var isPublishing = isPublished ? true : publishingCheck(mandatoryVariant); + var isPublishing = isPublished || publishingCheck(mandatoryVariant); if (isPublished || isPublishing) continue; @@ -1257,7 +1262,7 @@ namespace Umbraco.Web.Editors } /// - /// This will call PublishCulture on the content item for each culture that needs to be published including the invariant culture + /// Call PublishCulture on the content item for each culture to get a validation result for each culture /// /// /// @@ -1314,7 +1319,7 @@ namespace Umbraco.Web.Editors return HandleContentNotFound(id, false); } - var publishResult = Services.ContentService.SavePublishing(foundContent, Security.GetUserId().ResultOr(0)); + var publishResult = Services.ContentService.SaveAndPublish(foundContent, userId: Security.GetUserId().ResultOr(0)); if (publishResult.Success == false) { var notificationModel = new SimpleNotificationModel(); diff --git a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs index 2a3bca9c16..0ce317ac65 100644 --- a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs +++ b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs @@ -252,8 +252,8 @@ namespace Umbraco.Web.Macros if (_cultureInfos != null) return _cultureInfos; - return _cultureInfos = _inner.PublishCultureInfos - .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.Date)); + return _cultureInfos = _inner.PublishCultureInfos.Values + .ToDictionary(x => x.Culture, x => new PublishedCultureInfo(x.Culture, x.Name, x.Date)); } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs index 0181fc25be..0e324c94b9 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Models.Mapping where TPropertyTypeBasic : PropertyTypeBasic { return mapping - .ConstructUsing(x => new PropertyGroup(false)) // TODO: we have NO idea of isPublishing here = wtf? + .ConstructUsing(x => new PropertyGroup(false)) // TODO: we have NO idea of isPublishing here = so what? .IgnoreEntityCommonProperties() .ForMember(dest => dest.Id, map => map.Condition(src => src.Id > 0)) .ForMember(dest => dest.Key, map => map.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs index dbdac1f91f..af894bee5e 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs @@ -56,6 +56,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.FailedPasswordAttempts, opt => opt.Ignore()) .ForMember(dest => dest.DeleteDate, opt => opt.Ignore()) .ForMember(dest => dest.WriterId, opt => opt.Ignore()) + .ForMember(dest => dest.VersionId, opt => opt.Ignore()) // TODO: Support these eventually .ForMember(dest => dest.PasswordQuestion, opt => opt.Ignore()) .ForMember(dest => dest.RawPasswordAnswerValue, opt => opt.Ignore()); diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 7ff6439e08..6dee2f78b5 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -172,18 +172,16 @@ namespace Umbraco.Web.PropertyEditors try { // create a temp property with the value + // - force it to be culture invariant as NC can't handle culture variant element properties + propType.Variations = ContentVariation.Nothing; var tempProp = new Property(propType); - // if the property varies by culture, make sure we save using the current culture - var propCulture = propType.VariesByCulture() || propType.VariesByCultureAndSegment() - ? culture - : null; - tempProp.SetValue(propValues[propAlias] == null ? null : propValues[propAlias].ToString(), propCulture); + tempProp.SetValue(propValues[propAlias] == null ? null : propValues[propAlias].ToString()); // convert that temp property, and store the converted value var propEditor = _propertyEditors[propType.PropertyEditorAlias]; var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ToEditor(tempProp, dataTypeService, propCulture); + var convValue = valEditor.ToEditor(tempProp, dataTypeService); propValues[propAlias] = convValue == null ? null : JToken.FromObject(convValue); } catch (InvalidOperationException) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 18324a473d..9845becb45 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1239,10 +1239,11 @@ namespace Umbraco.Web.PublishedCache.NuCache : document.CultureInfos) : content.CultureInfos; - foreach (var (culture, info) in infos) + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in infos) { - var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(culture); - cultureData[culture] = new CultureVariation { Name = info.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue, IsDraft = cultureIsDraft }; + var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture); + cultureData[cultureInfo.Culture] = new CultureVariation { Name = cultureInfo.Name, Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue, IsDraft = cultureIsDraft }; } }