diff --git a/src/Umbraco.Core/Components/RelateOnCopyComponent.cs b/src/Umbraco.Core/Components/RelateOnCopyComponent.cs index 4ebd309e9f..5356fa6e30 100644 --- a/src/Umbraco.Core/Components/RelateOnCopyComponent.cs +++ b/src/Umbraco.Core/Components/RelateOnCopyComponent.cs @@ -1,10 +1,12 @@ using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Components { + //TODO: This should just exist in the content service/repo! [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public sealed class RelateOnCopyComponent : UmbracoComponentBase, IUmbracoCoreComponent diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 6f90b5201d..9e73205c36 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -363,22 +363,28 @@ namespace Umbraco.Core.Models /// Alias of the to remove public void RemovePropertyType(string propertyTypeAlias) { - //check if the property exist in one of our collections - if (PropertyGroups.Any(group => group.PropertyTypes.Any(pt => pt.Alias == propertyTypeAlias)) - || _propertyTypes.Any(x => x.Alias == propertyTypeAlias)) - { - //set the flag that a property has been removed - HasPropertyTypeBeenRemoved = true; - } - + //check through each property group to see if we can remove the property type by alias from it foreach (var propertyGroup in PropertyGroups) { - propertyGroup.PropertyTypes.RemoveItem(propertyTypeAlias); + if (propertyGroup.PropertyTypes.RemoveItem(propertyTypeAlias)) + { + if (!HasPropertyTypeBeenRemoved) + { + HasPropertyTypeBeenRemoved = true; + OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + } + break; + } } - if (_propertyTypes.Any(x => x.Alias == propertyTypeAlias)) + //check through each local property type collection (not assigned to a tab) + if (_propertyTypes.RemoveItem(propertyTypeAlias)) { - _propertyTypes.RemoveItem(propertyTypeAlias); + if (!HasPropertyTypeBeenRemoved) + { + HasPropertyTypeBeenRemoved = true; + OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + } } } @@ -408,23 +414,9 @@ namespace Umbraco.Core.Models /// PropertyTypes that are not part of a PropertyGroup /// [IgnoreDataMember] + //fixme should we mark this as EditorBrowsable hidden since it really isn't ever used? internal PropertyTypeCollection PropertyTypeCollection => _propertyTypes; - /// - /// Indicates whether a specific property on the current entity is dirty. - /// - /// Name of the property to check - /// True if Property is dirty, otherwise False - public override bool IsPropertyDirty(string propertyName) - { - bool existsInEntity = base.IsPropertyDirty(propertyName); - - bool anyDirtyGroups = PropertyGroups.Any(x => x.IsPropertyDirty(propertyName)); - bool anyDirtyTypes = PropertyTypes.Any(x => x.IsPropertyDirty(propertyName)); - - return existsInEntity || anyDirtyGroups || anyDirtyTypes; - } - /// /// Indicates whether the current entity is dirty. /// diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index ef55f0d469..0d2f817660 100644 --- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs @@ -1,4 +1,8 @@ -using Umbraco.Core.Models.PublishedContent; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models { @@ -16,5 +20,63 @@ namespace Umbraco.Core.Models else if (typeof(IMemberType).IsAssignableFrom(type)) itemType = PublishedItemType.Member; return itemType; } + + /// + /// Used to check if any property type was changed between variant/invariant + /// + /// + /// + internal static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType) + { + return contentType.WasPropertyTypeVariationChanged(out var _); + } + + /// + /// Used to check if any property type was changed between variant/invariant + /// + /// + /// + internal static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType, out IReadOnlyCollection aliases) + { + var a = new List(); + + // property variation change? + var hasAnyPropertyVariationChanged = contentType.PropertyTypes.Any(propertyType => + { + if (!(propertyType is IRememberBeingDirty dirtyProperty)) + throw new Exception("oops"); + + // skip new properties + //TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly + var isNewProperty = dirtyProperty.WasPropertyDirty("Id"); + if (isNewProperty) return false; + + // variation change? + var dirty = dirtyProperty.WasPropertyDirty("Variations"); + if (dirty) + a.Add(propertyType.Alias); + + return dirty; + + }); + + aliases = a; + return hasAnyPropertyVariationChanged; + } + + /// + /// Returns the list of content types the composition is used in + /// + /// + /// + /// + internal static IEnumerable GetWhereCompositionIsUsedInContentTypes(this IContentTypeComposition source, + IContentTypeComposition[] allContentTypes) + { + var sourceId = source != null ? source.Id : 0; + + // find which content types are using this composition + return allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); + } } } diff --git a/src/Umbraco.Core/Models/IContentTypeComposition.cs b/src/Umbraco.Core/Models/IContentTypeComposition.cs index b5277a23be..36ace19f0f 100644 --- a/src/Umbraco.Core/Models/IContentTypeComposition.cs +++ b/src/Umbraco.Core/Models/IContentTypeComposition.cs @@ -10,6 +10,7 @@ namespace Umbraco.Core.Models /// /// Gets or sets the content types that compose this content type. /// + //fixme: we should be storing key references, not the object else we are caching way too much IEnumerable ContentTypeComposition { get; set; } /// diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 53b75f7e48..47710e04cb 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -124,10 +124,11 @@ namespace Umbraco.Core.Models return this.Any(x => x.Alias == propertyAlias); } - public void RemoveItem(string propertyTypeAlias) + public bool RemoveItem(string propertyTypeAlias) { var key = IndexOfKey(propertyTypeAlias); if (key != -1) RemoveItem(key); + return key != -1; } public int IndexOfKey(string key) diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyDataDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyDataDto.cs index cb47784d92..a3b28b5b54 100644 --- a/src/Umbraco.Core/Persistence/Dtos/PropertyDataDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/PropertyDataDto.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Dtos [ExplicitColumns] internal class PropertyDataDto { - private const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; public const int VarcharLength = 512; public const int SegmentLength = 256; diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index d97c748b6f..4fdc72f52f 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -1036,7 +1036,7 @@ namespace Umbraco.Core.Persistence { var pd = sql.SqlContext.PocoDataFactory.ForType(typeof (TDto)); var tableName = tableAlias ?? pd.TableInfo.TableName; - var queryColumns = pd.QueryColumns; + var queryColumns = pd.QueryColumns.ToList(); Dictionary aliases = null; @@ -1056,7 +1056,11 @@ namespace Umbraco.Core.Persistence return fieldName; }).ToArray(); - queryColumns = queryColumns.Where(x => names.Contains(x.Key)).ToArray(); + //only get the columns that exist in the selected names + queryColumns = queryColumns.Where(x => names.Contains(x.Key)).ToList(); + + //ensure the order of the columns in the expressions is the order in the result + queryColumns.Sort((a, b) => names.IndexOf(a.Key).CompareTo(names.IndexOf(b.Key))); } string GetAlias(PocoColumn column) diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index f5e9013082..3bb1ac38ca 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -10,6 +10,12 @@ namespace Umbraco.Core.Persistence.Repositories { TItem Get(string alias); IEnumerable> Move(TItem moving, EntityContainer container); + + /// + /// Returns the content types that are direct compositions of the content type + /// + /// The content type id + /// IEnumerable GetTypesDirectlyComposedOf(int id); /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 6ace73fbc3..c258a76b30 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -608,6 +608,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region UnitOfWork Events + //fixme: The reason these events are in the repository is for legacy, the events should exist at the service + // level now since we can fire these events within the transaction... so move the events to service level + public class ScopedEntityEventArgs : EventArgs { public ScopedEntityEventArgs(IScope scope, TEntity entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 555fc56157..3f1ea3116e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -274,6 +274,8 @@ AND umbracoNode.id <> @id", if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null && compositionBase.RemovedContentTypeKeyTracker.Any()) { + //TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? + // find Content based on the current ContentType var sql = Sql() .SelectAll() @@ -292,6 +294,7 @@ AND umbracoNode.id <> @id", // based on the PropertyTypes that belong to the removed ContentType. foreach (var contentDto in contentDtos) { + //TODO: This could be done with bulk SQL statements foreach (var propertyType in propertyTypes) { var nodeId = contentDto.NodeId; @@ -323,9 +326,7 @@ AND umbracoNode.id <> @id", }); } - // fixme below, manage the property type - - // delete ??? fixme wtf is this? + // delete property types // ... by excepting entries from db with entries from collections if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) { @@ -404,10 +405,49 @@ AND umbracoNode.id <> @id", propertyType.PropertyGroupId = new Lazy(() => groupId); } + //check if the content type variation has been changed + var ctVariationChanging = entity.IsPropertyDirty("Variations"); + if (ctVariationChanging) + { + //we've already looked up the previous version of the content type so we know it's previous variation state + MoveVariantData(entity, (ContentVariation)dtoPk.Variations, entity.Variations); + Clear301Redirects(entity); + ClearScheduledPublishing(entity); + } + + //track any content type/property types that are changing variation which will require content updates + var propertyTypeVariationChanges = new Dictionary(); + // insert or update properties // all of them, no-group and in-groups foreach (var propertyType in entity.PropertyTypes) { + //if the content type variation isn't changing track if any property type is changing + if (!ctVariationChanging) + { + if (propertyType.IsPropertyDirty("Variations")) + { + propertyTypeVariationChanges[propertyType.Id] = propertyType.Variations; + } + } + else + { + switch(entity.Variations) + { + case ContentVariation.Nothing: + //if the content type is changing to Nothing, then all property type's must change to nothing + propertyType.Variations = ContentVariation.Nothing; + break; + case ContentVariation.Culture: + //we don't need to modify the property type in this case + break; + case ContentVariation.CultureAndSegment: + case ContentVariation.Segment: + default: + throw new NotSupportedException(); //TODO: Support this + } + } + var groupId = propertyType.PropertyGroupId?.Value ?? default(int); // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) @@ -431,6 +471,28 @@ AND umbracoNode.id <> @id", orphanPropertyTypeIds.Remove(typeId); } + //check if any property types were changing variation + if (propertyTypeVariationChanges.Count > 0) + { + var changes = new Dictionary(); + + //now get the current property type variations for the changed ones so that we know which variation they + //are going from and to + var from = Database.Dictionary(Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypeVariationChanges.Keys)); + + foreach (var f in from) + { + changes[f.Key] = (propertyTypeVariationChanges[f.Key], (ContentVariation)f.Value); + } + + //perform the move + MoveVariantData(changes); + } + + // deal with orphan properties: those that were in a deleted tab, // and have not been re-mapped to another tab or to 'generic properties' if (orphanPropertyTypeIds != null) @@ -438,6 +500,221 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, id); } + /// + /// Clear any redirects associated with content for a content type + /// + private void Clear301Redirects(IContentTypeComposition contentType) + { + //first clear out any existing property data that might already exists under the default lang + var sqlSelect = Sql().Select(x => x.UniqueId) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + var sqlDelete = Sql() + .Delete() + .WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), sqlSelect); + + Database.Execute(sqlDelete); + } + + /// + /// Clear any scheduled publishing associated with content for a content type + /// + private void ClearScheduledPublishing(IContentTypeComposition contentType) + { + //TODO: Fill this in when scheduled publishing is enabled for variants + } + + /// + /// Moves variant data for property type changes + /// + /// + private void MoveVariantData(IDictionary propertyTypeChanges) + { + var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + + //Group by the "To" variation so we can bulk update in the correct batches + foreach(var g in propertyTypeChanges.GroupBy(x => x.Value.Item2)) + { + var propertyTypeIds = g.Select(s => s.Key).ToList(); + + //the ContentVariation that the data is moving "To" + var toVariantType = g.Key; + + switch(toVariantType) + { + case ContentVariation.Culture: + MovePropertyDataToVariantCulture(defaultLangId, propertyTypeIds: propertyTypeIds); + break; + case ContentVariation.Nothing: + MovePropertyDataToVariantNothing(defaultLangId, propertyTypeIds: propertyTypeIds); + break; + case ContentVariation.CultureAndSegment: + case ContentVariation.Segment: + default: + throw new NotSupportedException(); //TODO: Support this + } + } + } + + /// + /// Moves variant data for a content type variation change + /// + /// + /// + /// + private void MoveVariantData(IContentTypeComposition contentType, ContentVariation from, ContentVariation to) + { + var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + + var sqlPropertyTypeIds = Sql().Select(x => x.Id).From().Where(x => x.ContentTypeId == contentType.Id); + switch (to) + { + case ContentVariation.Culture: + //move the property data + + MovePropertyDataToVariantCulture(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); + + //now we need to move the names + //first clear out any existing names that might already exists under the default lang + //there's 2x tables to update + + //clear out the versionCultureVariation table + var sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLangId); + var sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); + + //clear out the documentCultureVariation table + sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLangId); + sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); + + //now we need to insert names into these 2 tables based on the invariant data + + //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang + var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) + .Append($", {defaultLangId}") //default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); + + //insert rows into the documentCultureVariation table + cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) + .AndSelect(x => x.Text) + .Append($", 1, {defaultLangId}") //make Available + default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); + + break; + case ContentVariation.Nothing: + //move the property data + + MovePropertyDataToVariantNothing(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); + + //we dont need to move the names! this is because we always keep the invariant names with the name of the default language. + + //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( + // if we want these SQL statements back, look into GIT history + + break; + case ContentVariation.CultureAndSegment: + case ContentVariation.Segment: + default: + throw new NotSupportedException(); //TODO: Support this + } + } + + /// + /// This will move all property data from variant to invariant + /// + /// + /// Optional list of property type ids of the properties to be updated + /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated + private void MovePropertyDataToVariantNothing(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) + { + //first clear out any existing property data that might already exists under the default lang + var sqlDelete = Sql() + .Delete() + .Where(x => x.LanguageId == null); + if (sqlPropertyTypeIds != null) + sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); + if (propertyTypeIds != null) + sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + Database.Execute(sqlDelete); + + //now insert all property data into the default language that exists under the invariant lang + var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); + var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) + .Append(", NULL") //null language ID + .From() + .Where(x => x.LanguageId == defaultLangId); + if (sqlPropertyTypeIds != null) + sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); + if (propertyTypeIds != null) + sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + + Database.Execute(sqlInsert); + } + + /// + /// This will move all property data from invariant to variant + /// + /// + /// Optional list of property type ids of the properties to be updated + /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated + private void MovePropertyDataToVariantCulture(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) + { + //first clear out any existing property data that might already exists under the default lang + var sqlDelete = Sql() + .Delete() + .Where(x => x.LanguageId == defaultLangId); + if (sqlPropertyTypeIds != null) + sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); + if (propertyTypeIds != null) + sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + Database.Execute(sqlDelete); + + //now insert all property data into the default language that exists under the invariant lang + var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); + var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) + .Append($", {defaultLangId}") //default language ID + .From() + .Where(x => x.LanguageId == null); + if (sqlPropertyTypeIds != null) + sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); + if (propertyTypeIds != null) + sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + + Database.Execute(sqlInsert); + } + private void DeletePropertyType(int contentTypeId, int propertyTypeId) { // first clear dependencies @@ -571,8 +848,11 @@ AND umbracoNode.id <> @id", } } + /// public IEnumerable GetTypesDirectlyComposedOf(int id) { + //fixme - this will probably be more efficient to simply load all content types and do the calculation, see GetWhereCompositionIsUsedInContentTypes + var sql = Sql() .SelectAll() .From() diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 2b3674700b..e316d1d04b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); } - private FullDataSetRepositoryCachePolicy TypedCachePolicy => (FullDataSetRepositoryCachePolicy) CachePolicy; + private FullDataSetRepositoryCachePolicy TypedCachePolicy => CachePolicy as FullDataSetRepositoryCachePolicy; #region Overrides of RepositoryBase @@ -225,7 +225,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public ILanguage GetByIsoCode(string isoCode) { - TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) + TypedCachePolicy.GetAllCached(PerformGetAll); + + var id = GetIdByIsoCode(isoCode, throwOnNotFound: false); return id.HasValue ? Get(id.Value) : null; } @@ -238,7 +242,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { if (isoCode == null) return null; - TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) + TypedCachePolicy.GetAllCached(PerformGetAll); + else + PerformGetAll(); //we don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap + lock (_codeIdMap) { if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; @@ -256,7 +265,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { if (id == null) return null; - TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) + TypedCachePolicy.GetAllCached(PerformGetAll); + else + PerformGetAll(); + lock (_codeIdMap) // yes, we want to lock _codeIdMap { if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) return isoCode; @@ -279,8 +293,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // do NOT leak that language, it's not deep-cloned! private ILanguage GetDefault() { - // get all cached, non-cloned - var languages = TypedCachePolicy.GetAllCached(PerformGetAll).ToList(); + // get all cached + var languages = (TypedCachePolicy?.GetAllCached(PerformGetAll) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests) + ?? CachePolicy.GetAll(Array.Empty(), PerformGetAll)).ToList(); + var language = languages.FirstOrDefault(x => x.IsDefault); if (language != null) return language; diff --git a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs index 762b6dd7dd..4df11e4f60 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.PropertyEditors.Validators /// public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) { - var asString = value.ToString(); + var asString = value == null ? "" : value.ToString(); var emailVal = new EmailAddressAttribute(); diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 9e82213aa5..66b3982b49 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -109,23 +109,7 @@ namespace Umbraco.Core.Services return new ContentTypeAvailableCompositionsResults(ancestors, result); } - /// - /// Returns the list of content types the composition is used in - /// - /// - /// - /// - /// - internal static IEnumerable GetWhereCompositionIsUsedInContentTypes(this IContentTypeService ctService, - IContentTypeComposition source, - IContentTypeComposition[] allContentTypes) - { - - var sourceId = source != null ? source.Id : 0; - - // find which content types are using this composition - return allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); - } + private static IContentTypeComposition[] GetAncestors(IContentTypeComposition ctype, IContentTypeComposition[] allContentTypes) { diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs index 9fa9a47003..33fb9a0894 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs @@ -20,12 +20,12 @@ namespace Umbraco.Core.Services.Implement internal static event TypedEventHandler.EventArgs> Changed; // that one is always immediate (transactional) - public static event TypedEventHandler.EventArgs> UowRefreshedEntity; + public static event TypedEventHandler.EventArgs> ScopedRefreshedEntity; // used by tests to clear events internal static void ClearScopeEvents() { - UowRefreshedEntity = null; + ScopedRefreshedEntity = null; } // these must be dispatched @@ -48,7 +48,7 @@ namespace Umbraco.Core.Services.Implement protected void OnUowRefreshedEntity(ContentTypeChange.EventArgs args) { // that one is always immediate (not dispatched, transactional) - UowRefreshedEntity.RaiseEvent(args, This); + ScopedRefreshedEntity.RaiseEvent(args, This); } protected bool OnSavingCancelled(IScope scope, SaveEventArgs args) diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 8ed0a0f645..a114f415cc 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -118,6 +118,8 @@ namespace Umbraco.Core.Services.Implement // - content type alias changed // - content type property removed, or alias changed // - content type composition removed (not testing if composition had properties...) + // - content type variation changed + // - property type variation changed // // because these are the changes that would impact the raw content data @@ -132,7 +134,8 @@ namespace Umbraco.Core.Services.Implement var dirty = (IRememberBeingDirty)contentType; // skip new content types - var isNewContentType = dirty.WasPropertyDirty("HasIdentity"); + //TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly + var isNewContentType = dirty.WasPropertyDirty("Id"); if (isNewContentType) { AddChange(changes, contentType, ContentTypeChangeTypes.Create); @@ -149,12 +152,12 @@ namespace Umbraco.Core.Services.Implement throw new Exception("oops"); // skip new properties - var isNewProperty = dirtyProperty.WasPropertyDirty("HasIdentity"); + //TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly + var isNewProperty = dirtyProperty.WasPropertyDirty("Id"); if (isNewProperty) return false; // alias change? - var hasPropertyAliasBeenChanged = dirtyProperty.WasPropertyDirty("Alias"); - return hasPropertyAliasBeenChanged; + return dirtyProperty.WasPropertyDirty("Alias"); }); // removed properties? @@ -163,8 +166,15 @@ namespace Umbraco.Core.Services.Implement // removed compositions? var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved"); + // variation changed? + var hasContentTypeVariationChanged = dirty.WasPropertyDirty("Variations"); + + // property variation change? + var hasAnyPropertyVariationChanged = contentType.WasPropertyTypeVariationChanged(); + // main impact on properties? - var hasPropertyMainImpact = hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias; + var hasPropertyMainImpact = hasContentTypeVariationChanged || hasAnyPropertyVariationChanged + || hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias; if (hasAliasChanged || hasPropertyMainImpact) { @@ -336,6 +346,9 @@ namespace Umbraco.Core.Services.Implement public IEnumerable GetComposedOf(int id) { + //fixme: this is essentially the same as ContentTypeServiceExtensions.GetWhereCompositionIsUsedInContentTypes which loads + // all content types to figure this out, this instead makes quite a few queries so should be replaced + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(ReadLockIds); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 31105aa28e..6087279285 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -63,8 +63,7 @@ namespace Umbraco.Tests.Persistence.Repositories } //TODO Add test to verify SetDefaultTemplates updates both AllowedTemplates and DefaultTemplate(id). - - + [Test] public void Maps_Templates_Correctly() { @@ -377,7 +376,7 @@ namespace Umbraco.Tests.Persistence.Repositories repository.Save(contentType); - var dirty = ((ICanBeDirty)contentType).IsDirty(); + var dirty = contentType.IsDirty(); // Assert Assert.That(contentType.HasIdentity, Is.True); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs similarity index 99% rename from src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs rename to src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 7ff7c0a2e4..68e29c4efe 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -25,7 +25,7 @@ namespace Umbraco.Tests.Persistence.Repositories { [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] - public class ContentRepositoryTest : TestWithDatabaseBase + public class DocumentRepositoryTest : TestWithDatabaseBase { public override void SetUp() { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index be07dbdd23..f25382d557 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Services.Implement; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; +using Umbraco.Core.Components; namespace Umbraco.Tests.Services { @@ -23,6 +24,361 @@ namespace Umbraco.Tests.Services [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true)] public class ContentTypeServiceTests : TestWithSomeContentBase { + [Test] + public void Change_Content_Type_Variation_Clears_Redirects() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Nothing + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Hello1"; + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + ServiceContext.ContentService.Save(doc2); + + ServiceContext.RedirectUrlService.Register("hello/world", doc.Key); + ServiceContext.RedirectUrlService.Register("hello2/world2", doc2.Key); + + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); + + //change variation + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + Assert.AreEqual(0, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); + + } + + [Test] + public void Change_Content_Type_From_Invariant_Variant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Nothing + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Hello1"; + doc.SetValue("title", "hello world"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("Hello1", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change the content type to be variant, we will also update the name here to detect the copy changes + doc.Name = "Hello2"; + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello2", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title")); //We are not checking against en-US here because properties will remain invariant + + //change back property type to be invariant, we will also update the name here to detect the copy changes + doc.SetCultureName("Hello3", "en-US"); + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello3", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + } + + [Test] + public void Change_Content_Type_From_Variant_Invariant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Hello1", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("Hello1", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + //change the content type to be invariant, we will also update the name here to detect the copy changes + doc.SetCultureName("Hello2", "en-US"); + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello2", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change back property type to be variant, we will also update the name here to detect the copy changes + doc.Name = "Hello3"; + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + //at this stage all property types were switched to invariant so even though the variant value + //exists it will not be returned because the property type is invariant, + //so this check proves that null will be returned + Assert.IsNull(doc.GetValue("title", "en-US")); + + //we can now switch the property type to be variant and the value can be returned again + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello3", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + } + + [Test] + public void Change_Property_Type_From_Invariant_Variant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Nothing + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Home"; + doc.SetValue("title", "hello world"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change the property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + //change back property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + } + + [Test] + public void Change_Property_Type_From_Variant_Invariant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + //change the property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change back property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + } + + [Test] + public void Change_Property_Type_From_Variant_Invariant_On_A_Composition() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //compose this from the other one + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + contentType2.Variations = ContentVariation.Culture; + contentType2.AddContentType(contentType); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + doc2.SetCultureName("Home", "en-US"); + doc2.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc2); + + //change the property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.AreEqual("hello world", doc2.GetValue("title")); + + //change back property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.AreEqual("hello world", doc2.GetValue("title", "en-US")); + } + + [Test] + public void Change_Content_Type_From_Variant_Invariant_On_A_Composition() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //compose this from the other one + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + contentType2.Variations = ContentVariation.Culture; + contentType2.AddContentType(contentType); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + doc2.SetCultureName("Home", "en-US"); + doc2.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc2); + + //change the content type to be invariant + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.AreEqual("hello world", doc2.GetValue("title")); + + //change back content type to be variant + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + //this will be null because the doc type was changed back to variant but it's property types don't get changed back + Assert.IsNull(doc.GetValue("title", "en-US")); + Assert.IsNull(doc2.GetValue("title", "en-US")); + } + [Test] public void Deleting_Media_Type_With_Hierarchy_Of_Media_Items_Moves_Orphaned_Media_To_Recycle_Bin() { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 0d346f74dd..61ae537529 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -123,6 +123,7 @@ + @@ -391,7 +392,6 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index aec667077c..efb63fe1ac 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -99,7 +99,7 @@ evts.push(eventsService.on("editors.documentType.saved", function (name, args) { // if this content item uses the updated doc type we need to reload the content item - if (args && args.documentType && args.documentType.key === content.documentType.key) { + if (args && args.documentType && args.documentType.key === $scope.content.documentType.key) { loadContent(); } })); diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index fa1aaf7345..22d86631ca 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -161,7 +161,7 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - var contentTypesWhereCompositionIsUsed = Services.ContentTypeService.GetWhereCompositionIsUsedInContentTypes(source, allContentTypes); + var contentTypesWhereCompositionIsUsed = source.GetWhereCompositionIsUsedInContentTypes(allContentTypes); return contentTypesWhereCompositionIsUsed .Select(x => Mapper.Map(x)) .Select(x => diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 8786753e4f..a9903669b9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -198,6 +198,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private void InitializeRepositoryEvents() { + //fixme: The reason these events are in the repository is for legacy, the events should exist at the service + // level now since we can fire these events within the transaction... so move the events to service level + // plug repository event handlers // these trigger within the transaction to ensure consistency // and are used to maintain the central, database-level XML cache @@ -212,9 +215,9 @@ namespace Umbraco.Web.PublishedCache.NuCache MemberRepository.ScopedEntityRefresh += OnMemberRefreshedEntity; // plug - ContentTypeService.UowRefreshedEntity += OnContentTypeRefreshedEntity; - MediaTypeService.UowRefreshedEntity += OnMediaTypeRefreshedEntity; - MemberTypeService.UowRefreshedEntity += OnMemberTypeRefreshedEntity; + ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; + MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; + MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; } private void TearDownRepositoryEvents() @@ -229,9 +232,9 @@ namespace Umbraco.Web.PublishedCache.NuCache //MemberRepository.RemovedVersion -= OnMemberRemovedVersion; MemberRepository.ScopedEntityRefresh -= OnMemberRefreshedEntity; - ContentTypeService.UowRefreshedEntity -= OnContentTypeRefreshedEntity; - MediaTypeService.UowRefreshedEntity -= OnMediaTypeRefreshedEntity; - MemberTypeService.UowRefreshedEntity -= OnMemberTypeRefreshedEntity; + ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; + MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; + MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; } public override void Dispose() diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs index 5fa89e3f7b..5b816c2f26 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs @@ -194,9 +194,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache MemberRepository.ScopedEntityRefresh += OnMemberRefreshedEntity; // plug - ContentTypeService.UowRefreshedEntity += OnContentTypeRefreshedEntity; - MediaTypeService.UowRefreshedEntity += OnMediaTypeRefreshedEntity; - MemberTypeService.UowRefreshedEntity += OnMemberTypeRefreshedEntity; + ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; + MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; + MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; _withRepositoryEvents = true; } @@ -213,9 +213,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache MemberRepository.ScopeVersionRemove -= OnMemberRemovingVersion; MemberRepository.ScopedEntityRefresh -= OnMemberRefreshedEntity; - ContentTypeService.UowRefreshedEntity -= OnContentTypeRefreshedEntity; - MediaTypeService.UowRefreshedEntity -= OnMediaTypeRefreshedEntity; - MemberTypeService.UowRefreshedEntity -= OnMemberTypeRefreshedEntity; + ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; + MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; + MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; _withRepositoryEvents = false; }