Merge branch 'temp8' into temp8-contentAndType
# Conflicts: # src/Umbraco.Core/Models/Content.cs # src/Umbraco.Core/Models/ContentBase.cs # src/Umbraco.Tests/Models/ContentTests.cs
This commit is contained in:
@@ -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<string> _editedCultures;
|
||||
private ContentCultureInfosCollection _publishInfos;
|
||||
|
||||
#region Used for change tracking
|
||||
|
||||
private (HashSet<string> addedCultures, HashSet<string> removedCultures, HashSet<string> updatedCultures) _currentPublishCultureChanges;
|
||||
private (HashSet<string> addedCultures, HashSet<string> removedCultures, HashSet<string> updatedCultures) _previousPublishCultureChanges;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this content item is published or not.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
[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; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public DateTime? PublishDate { get; internal set; } // set by persistence
|
||||
public DateTime? PublishDate { get; set; } // set by persistence
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public int? PublisherId { get; internal set; } // set by persistence
|
||||
public int? PublisherId { get; set; } // set by persistence
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public int? PublishTemplateId { get; internal set; } // set by persistence
|
||||
public int? PublishTemplateId { get; set; } // set by persistence
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public string PublishName { get; internal set; } // set by persistence
|
||||
public string PublishName { get; set; } // set by persistence
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public IEnumerable<string> EditedCultures => CultureInfos.Keys.Where(IsCultureEdited);
|
||||
public IEnumerable<string> EditedCultures
|
||||
{
|
||||
get => CultureInfos.Keys.Where(IsCultureEdited);
|
||||
set => _editedCultures = value == null ? null : new HashSet<string>(value, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
@@ -193,32 +204,6 @@ namespace Umbraco.Core.Models
|
||||
// a non-available culture could not become published anyways
|
||||
=> _publishInfos != null && _publishInfos.ContainsKey(culture);
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsCultureEdited(string culture)
|
||||
=> IsCultureAvailable(culture) && // is available, and
|
||||
@@ -227,7 +212,23 @@ namespace Umbraco.Core.Models
|
||||
|
||||
/// <inheritdoc/>
|
||||
[IgnoreDataMember]
|
||||
public IReadOnlyDictionary<string, ContentCultureInfos> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<string>(StringComparer.OrdinalIgnoreCase);
|
||||
_editedCultures.Add(culture.ToLowerInvariant());
|
||||
}
|
||||
|
||||
// sets all publish edited
|
||||
internal void SetCultureEdited(IEnumerable<string> cultures)
|
||||
{
|
||||
if (cultures == null)
|
||||
{
|
||||
_editedCultures = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var editedCultures = new HashSet<string>(cultures.Where(x => !x.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase);
|
||||
_editedCultures = editedCultures.Count > 0 ? editedCultures : null;
|
||||
}
|
||||
return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?)null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<ContentCultureInfos>().First();
|
||||
if (_currentPublishCultureChanges.addedCultures == null) _currentPublishCultureChanges.addedCultures = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet<string>(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<ContentCultureInfos>().First();
|
||||
if (_currentPublishCultureChanges.removedCultures == null) _currentPublishCultureChanges.removedCultures = new HashSet<string>(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<ContentCultureInfos>().First();
|
||||
if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet<string>(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; }
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Changes the <see cref="ContentType"/> for the current content object
|
||||
/// </summary>
|
||||
/// <param name="contentType">New ContentType for this content</param>
|
||||
/// <remarks>Leaves PropertyTypes intact after change</remarks>
|
||||
public void ChangeContentType(IContentType contentType)
|
||||
internal void ChangeContentType(IContentType contentType)
|
||||
{
|
||||
ChangeContentType(contentType, false);
|
||||
}
|
||||
@@ -411,7 +312,7 @@ namespace Umbraco.Core.Models
|
||||
/// </summary>
|
||||
/// <param name="contentType">New ContentType for this content</param>
|
||||
/// <param name="clearProperties">Boolean indicating whether to clear PropertyTypes upon change</param>
|
||||
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<string>(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase);
|
||||
_previousPublishCultureChanges.removedCultures = _currentPublishCultureChanges.removedCultures == null || _currentPublishCultureChanges.removedCultures.Count == 0 ? null : new HashSet<string>(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase);
|
||||
_previousPublishCultureChanges.updatedCultures = _currentPublishCultureChanges.updatedCultures == null || _currentPublishCultureChanges.updatedCultures.Count == 0 ? null : new HashSet<string>(_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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Overridden to check special keys.</remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Overridden to check special keys.</remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep clone of the current entity with its identity and it's property identities reset
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> addedCultures, HashSet<string> removedCultures, HashSet<string> updatedCultures) _currentCultureChanges;
|
||||
private (HashSet<string> addedCultures, HashSet<string> removedCultures, HashSet<string> updatedCultures) _previousCultureChanges;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentBase"/> class.
|
||||
/// </summary>
|
||||
@@ -74,24 +80,26 @@ namespace Umbraco.Core.Models
|
||||
OnPropertyChanged(nameof(Properties));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Id of the user who wrote/updated this entity
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Integer Id of the default ContentType
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public virtual int ContentTypeId
|
||||
public int ContentTypeId
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -114,11 +122,12 @@ namespace Umbraco.Core.Models
|
||||
/// </remarks>
|
||||
[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
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember]
|
||||
public virtual IReadOnlyDictionary<string, ContentCultureInfos> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles culture infos collection changes.
|
||||
/// </summary>
|
||||
private void CultureInfosCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(CultureInfos));
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
{
|
||||
var cultureInfo = e.NewItems.Cast<ContentCultureInfos>().First();
|
||||
if (_currentCultureChanges.addedCultures == null) _currentCultureChanges.addedCultures = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet<string>(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<ContentCultureInfos>().First();
|
||||
if (_currentCultureChanges.removedCultures == null) _currentCultureChanges.removedCultures = new HashSet<string>(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<ContentCultureInfos>().First();
|
||||
if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
_currentCultureChanges.updatedCultures.Add(cultureInfo.Culture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -243,11 +270,11 @@ namespace Umbraco.Core.Models
|
||||
#region Has, Get, Set, Publish Property Value
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool HasProperty(string propertyTypeAlias)
|
||||
public bool HasProperty(string propertyTypeAlias)
|
||||
=> Properties.Contains(propertyTypeAlias);
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual TValue GetValue<TValue>(string propertyTypeAlias, string culture = null, string segment = null, bool published = false)
|
||||
public TValue GetValue<TValue>(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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Overridden to include user properties.</remarks>
|
||||
public override void ResetDirtyProperties(bool rememberDirty)
|
||||
{
|
||||
base.ResetDirtyProperties(rememberDirty);
|
||||
|
||||
if (rememberDirty)
|
||||
{
|
||||
_previousCultureChanges.addedCultures = _currentCultureChanges.addedCultures == null || _currentCultureChanges.addedCultures.Count == 0 ? null : new HashSet<string>(_currentCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase);
|
||||
_previousCultureChanges.removedCultures = _currentCultureChanges.removedCultures == null || _currentCultureChanges.removedCultures.Count == 0 ? null : new HashSet<string>(_currentCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase);
|
||||
_previousCultureChanges.updatedCultures = _currentCultureChanges.updatedCultures == null || _currentCultureChanges.updatedCultures.Count == 0 ? null : new HashSet<string>(_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,6 @@ namespace Umbraco.Core.Models
|
||||
public ContentCultureInfosCollection()
|
||||
: base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentCultureInfosCollection"/> class with items.
|
||||
/// </summary>
|
||||
public ContentCultureInfosCollection(IEnumerable<ContentCultureInfos> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a <see cref="ContentCultureInfos"/> instance.
|
||||
@@ -53,7 +39,7 @@ namespace Umbraco.Core.Models
|
||||
Name = name,
|
||||
Date = date
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
292
src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
Normal file
292
src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Exceptions;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods used to manipulate content variations by the document repository
|
||||
/// </summary>
|
||||
internal static class ContentRepositoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the cultures that have been flagged for unpublishing.
|
||||
/// </summary>
|
||||
/// <remarks>Gets cultures for which content.UnpublishCulture() has been invoked.</remarks>
|
||||
public static IReadOnlyList<string> GetCulturesUnpublishing(this IContent content)
|
||||
{
|
||||
if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos"))
|
||||
return Array.Empty<string>();
|
||||
|
||||
var culturesUnpublishing = content.CultureInfos.Values
|
||||
.Where(x => content.IsPropertyDirty("_unpublishedCulture_" + x.Culture))
|
||||
.Select(x => x.Culture);
|
||||
|
||||
return culturesUnpublishing.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies values from another document.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the content item's properties pass variant rules
|
||||
/// </summary>
|
||||
/// <para>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.</para>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to synchronize all culture dates to the same date if they've been modified
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="date"></param>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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<string> cultures)
|
||||
{
|
||||
if (cultures == null)
|
||||
content.EditedCultures = null;
|
||||
else
|
||||
{
|
||||
var editedCultures = new HashSet<string>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a culture date, if the culture exists.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ namespace Umbraco.Core.Models.Entities
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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.
|
||||
|
||||
@@ -25,46 +25,46 @@ namespace Umbraco.Core.Models
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content is published.
|
||||
/// </summary>
|
||||
bool Published { get; }
|
||||
bool Published { get; set; }
|
||||
|
||||
PublishedState PublishedState { get; }
|
||||
PublishedState PublishedState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content has been edited.
|
||||
/// </summary>
|
||||
bool Edited { get; }
|
||||
bool Edited { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the published version identifier.
|
||||
/// </summary>
|
||||
int PublishedVersionId { get; }
|
||||
int PublishedVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content item is a blueprint.
|
||||
/// </summary>
|
||||
bool Blueprint { get; }
|
||||
bool Blueprint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the template id used to render the published version of the content.
|
||||
/// </summary>
|
||||
/// <remarks>When editing the content, the template can change, but this will not until the content is published.</remarks>
|
||||
int? PublishTemplateId { get; }
|
||||
int? PublishTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the published version of the content.
|
||||
/// </summary>
|
||||
/// <remarks>When editing the content, the name can change, but this will not until the content is published.</remarks>
|
||||
string PublishName { get; }
|
||||
string PublishName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier of the user who published the content.
|
||||
/// </summary>
|
||||
int? PublisherId { get; }
|
||||
int? PublisherId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date and time the content was published.
|
||||
/// </summary>
|
||||
DateTime? PublishDate { get; }
|
||||
DateTime? PublishDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.</para>
|
||||
/// <para>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).</para>
|
||||
/// even though the document might not have been saved yet (and can have no identity).</para>
|
||||
/// <para>Does not support the '*' wildcard (returns false).</para>
|
||||
/// </remarks>
|
||||
bool IsCulturePublished(string culture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a culture was published.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Mirrors <see cref="IsCulturePublished"/> whenever the content item is saved.</para>
|
||||
/// </remarks>
|
||||
bool WasCulturePublished(string culture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date a culture was published.
|
||||
/// </summary>
|
||||
@@ -120,7 +112,7 @@ namespace Umbraco.Core.Models
|
||||
/// <para>Because a dictionary key cannot be <c>null</c> this cannot get the invariant
|
||||
/// name, which must be get via the <see cref="PublishName"/> property.</para>
|
||||
/// </remarks>
|
||||
IReadOnlyDictionary<string, ContentCultureInfos> PublishCultureInfos { get; }
|
||||
ContentCultureInfosCollection PublishCultureInfos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the published cultures.
|
||||
@@ -130,47 +122,13 @@ namespace Umbraco.Core.Models
|
||||
/// <summary>
|
||||
/// Gets the edited cultures.
|
||||
/// </summary>
|
||||
IEnumerable<string> EditedCultures { get; }
|
||||
|
||||
// TODO: these two should move to some kind of service
|
||||
|
||||
/// <summary>
|
||||
/// Changes the <see cref="IContentType"/> for the current content object
|
||||
/// </summary>
|
||||
/// <param name="contentType">New ContentType for this content</param>
|
||||
/// <remarks>Leaves PropertyTypes intact after change</remarks>
|
||||
void ChangeContentType(IContentType contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the <see cref="IContentType"/> for the current content object and removes PropertyTypes,
|
||||
/// which are not part of the new ContentType.
|
||||
/// </summary>
|
||||
/// <param name="contentType">New ContentType for this content</param>
|
||||
/// <param name="clearProperties">Boolean indicating whether to clear PropertyTypes upon change</param>
|
||||
void ChangeContentType(IContentType contentType, bool clearProperties);
|
||||
IEnumerable<string> EditedCultures { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IContent DeepCloneWithResetIdentities();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a culture to be published.
|
||||
/// </summary>
|
||||
/// <returns>A value indicating whether the culture can be published.</returns>
|
||||
/// <remarks>
|
||||
/// <para>Fails if properties don't pass variant validation rules.</para>
|
||||
/// <para>Publishing must be finalized via the content service SavePublishing method.</para>
|
||||
/// </remarks>
|
||||
bool PublishCulture(string culture = "*");
|
||||
|
||||
/// <summary>
|
||||
/// Registers a culture to be unpublished.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Unpublishing must be finalized via the content service SavePublishing method.</para>
|
||||
/// </remarks>
|
||||
void UnpublishCulture(string culture = "*");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Umbraco.Core.Models
|
||||
/// <summary>
|
||||
/// Gets the version identifier.
|
||||
/// </summary>
|
||||
int VersionId { get; }
|
||||
int VersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the name of the content item for a specified culture.
|
||||
@@ -62,8 +62,8 @@ namespace Umbraco.Core.Models
|
||||
/// <para>Because a dictionary key cannot be <c>null</c> this cannot contain the invariant
|
||||
/// culture name, which must be get or set via the <see cref="TreeEntityBase.Name"/> property.</para>
|
||||
/// </remarks>
|
||||
IReadOnlyDictionary<string, ContentCultureInfos> CultureInfos { get; }
|
||||
|
||||
ContentCultureInfosCollection CultureInfos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the available cultures.
|
||||
/// </summary>
|
||||
@@ -125,16 +125,5 @@ namespace Umbraco.Core.Models
|
||||
/// <remarks>Values 'null' and 'empty' are equivalent for culture and segment.</remarks>
|
||||
void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null);
|
||||
|
||||
/// <summary>
|
||||
/// Copies values from another document.
|
||||
/// </summary>
|
||||
void CopyFrom(IContent other, string culture = "*");
|
||||
|
||||
/// <summary>
|
||||
/// Validates the content item's properties pass variant rules
|
||||
/// </summary>
|
||||
/// <para>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.</para>
|
||||
Property[] ValidateProperties(string culture = "*");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<object>(() => 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<object>(() => 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<object>(() => dataType.Configuration));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user