From 23333710f5b761b6eb34bcc10b89c98a34a2dcb2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 23 Apr 2018 12:53:17 +0200 Subject: [PATCH] Implement culture available/edited --- src/Umbraco.Core/EnumExtensions.cs | 22 + .../Install/DatabaseSchemaCreator.cs | 3 +- .../Migrations/Upgrade/UmbracoPlan.cs | 14 +- .../Upgrade/V_8_0_0/AddVariationTables1A.cs | 40 ++ ...riationTable.cs => AddVariationTables2.cs} | 8 +- src/Umbraco.Core/Models/Content.cs | 93 ++-- src/Umbraco.Core/Models/ContentBase.cs | 2 +- src/Umbraco.Core/Models/ContentTypeBase.cs | 2 +- src/Umbraco.Core/Models/IContent.cs | 6 + src/Umbraco.Core/Models/PropertyType.cs | 2 +- .../Persistence/Constants-DatabaseSchema.cs | 7 +- .../Dtos/ContentVersionCultureVariationDto.cs | 16 +- .../Dtos/DocumentCultureVariationDto.cs | 34 ++ .../Persistence/Factories/PropertyFactory.cs | 22 +- .../Implement/DocumentRepository.cs | 183 +++++-- .../Repositories/Implement/MediaRepository.cs | 4 +- .../Implement/MemberRepository.cs | 4 +- src/Umbraco.Core/Umbraco.Core.csproj | 5 +- src/Umbraco.Tests/Models/VariationTests.cs | 63 ++- .../Services/ContentServiceTests.cs | 500 +++++++++++------- .../Services/EntityServiceTests.cs | 8 +- src/Umbraco.Web/Editors/ContentController.cs | 3 +- .../Mapping/ContentItemDisplayNameResolver.cs | 10 +- .../Mapping/ContentTypeProfileExtensions.cs | 12 +- 24 files changed, 752 insertions(+), 311 deletions(-) create mode 100644 src/Umbraco.Core/EnumExtensions.cs create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs rename src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/{AddContentVariationTable.cs => AddVariationTables2.cs} (54%) create mode 100644 src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs new file mode 100644 index 0000000000..58e14bcadf --- /dev/null +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -0,0 +1,22 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Core +{ + /// + /// Provides extension methods for various enumerations. + /// + public static class EnumExtensions + { + /// + /// Determines whether a variation has all flags set. + /// + public static bool Has(this ContentVariation variation, ContentVariation values) + => (variation & values) == values; + + /// + /// Determines whether a variation has at least a flag set. + /// + public static bool HasAny(this ContentVariation variation, ContentVariation values) + => (variation & values) != ContentVariation.Unknown; + } +} diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 6ce300845c..7d4058a0e4 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -82,7 +82,8 @@ namespace Umbraco.Core.Migrations.Install typeof (UserLoginDto), typeof (ConsentDto), typeof (AuditEntryDto), - typeof (ContentVersionCultureVariationDto) + typeof (ContentVersionCultureVariationDto), + typeof (DocumentCultureVariationDto) }; /// diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 8bec74173e..4954742908 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -116,10 +116,11 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); Chain("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); Chain("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); - Chain("{66B6821A-0DE3-4DF8-A6A4-65ABD211EDDE}"); + Chain("{66B6821A-0DE3-4DF8-A6A4-65ABD211EDDE}"); + Chain("{49506BAE-CEBB-4431-A1A6-24AD6EBBBC57}"); // must chain to v8 final state (see at end of file) - Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // UPGRADE FROM 7, MORE RECENT @@ -201,12 +202,17 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{79591E91-01EA-43F7-AC58-7BD286DB1E77}"); // 8.0.0 - Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + // AddVariationTables1 has been superceeded by AddVariationTables2 + //Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); + + // however, need to take care of ppl in post-AddVariationTables1 state + Add("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // FINAL STATE - MUST MATCH LAST ONE ABOVE ! // whenever this changes, update all references in this file! - Add(string.Empty, "{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + Add(string.Empty, "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs new file mode 100644 index 0000000000..df77f34a2c --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs @@ -0,0 +1,40 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class AddVariationTables1A : MigrationBase + { + public AddVariationTables1A(IMigrationContext context) + : base(context) + { } + + // note - original AddVariationTables1 just did + // Create.Table().Do(); + // + // this is taking care of ppl left in this state + + public override void Migrate() + { + // note - original AddVariationTables1 just did + // Create.Table().Do(); + // + // it's been deprecated, not part of the main upgrade path, + // but we need to take care of ppl caught into the state + + // was not used + Delete.Column("available").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + // was not used + Delete.Column("availableDate").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + AddColumn("date"); + + // name, languageId are now non-nullable + AlterColumn(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "name"); + AlterColumn(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "languageId"); + + Create.Table().Do(); + + // fixme - data migration? + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs similarity index 54% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs index efd0f33e99..5b6c913195 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs @@ -1,16 +1,18 @@ -using Umbraco.Core.Persistence.Dtos; +using System; +using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { - public class AddContentVariationTable : MigrationBase + public class AddVariationTables2 : MigrationBase { - public AddContentVariationTable(IMigrationContext context) + public AddVariationTables2(IMigrationContext context) : base(context) { } public override void Migrate() { Create.Table().Do(); + Create.Table().Do(); // fixme - data migration? } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index bb2192f187..22c409d8b1 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -19,8 +19,9 @@ namespace Umbraco.Core.Models private bool _published; private PublishedState _publishedState; private DateTime? _releaseDate; - private DateTime? _expireDate; - private Dictionary _publishNames; + private DateTime? _expireDate; + private Dictionary _publishInfos; + private HashSet _edited; private static readonly Lazy Ps = new Lazy(); @@ -199,37 +200,40 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public string PublishName { get; internal set; } - /// - [IgnoreDataMember] - public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; - - /// - public string GetPublishName(string culture) + // sets publish infos + // internal for repositories + // clear by clearing name + internal void SetPublishInfos(string culture, string name, DateTime date) { - if (culture == null) return PublishName; - if (_publishNames == null) return null; - return _publishNames.TryGetValue(culture, out var name) ? name : null; - } - - // sets a publish name - // internal for repositories - internal void SetPublishName(string culture, string name) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullOrEmptyException(nameof(name)); if (culture == null) { PublishName = name; + PublishDate = date; return; } // private method, assume that culture is valid - if (_publishNames == null) - _publishNames = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (_publishInfos == null) + _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - _publishNames[culture] = name; + _publishInfos[culture] = (name, date); + } + + /// + [IgnoreDataMember] + //public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name) ?? NoNames; + + /// + public string GetPublishName(string culture) + { + if (culture == null) return PublishName; + if (_publishInfos == null) return null; + return _publishInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } // clears a publish name @@ -241,23 +245,51 @@ namespace Umbraco.Core.Models return; } - if (_publishNames == null) return; - _publishNames.Remove(culture); - if (_publishNames.Count == 0) - _publishNames = null; + if (_publishInfos == null) return; + _publishInfos.Remove(culture); + if (_publishInfos.Count == 0) + _publishInfos = null; } // clears all publish names private void ClearPublishNames() { PublishName = null; - _publishNames = null; + _publishInfos = null; } /// public bool IsCulturePublished(string culture) => !string.IsNullOrWhiteSpace(GetPublishName(culture)); + /// + public DateTime GetDateCulturePublished(string culture) + { + if (_publishInfos != null && _publishInfos.TryGetValue(culture, out var infos)) + return infos.Date; + throw new InvalidOperationException($"Culture \"{culture}\" is not published."); + } + + /// + public bool IsCultureEdited(string culture) + { + return string.IsNullOrWhiteSpace(GetPublishName(culture)) || (_edited != null && _edited.Contains(culture)); + } + + // sets a publish edited + internal void SetCultureEdited(string culture) + { + if (_edited == null) + _edited = new HashSet(StringComparer.OrdinalIgnoreCase); + _edited.Add(culture); + } + + // sets all publish edited + internal void SetCultureEdited(IEnumerable cultures) + { + _edited = new HashSet(cultures, StringComparer.OrdinalIgnoreCase); + } + [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -276,11 +308,12 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(Name)) throw new InvalidOperationException($"Cannot publish invariant culture without a name."); PublishName = Name; + var now = DateTime.Now; foreach (var (culture, name) in Names) { if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishName(culture, name); + SetPublishInfos(culture, name, now); } // property.PublishAllValues only deals with supported variations (if any) @@ -308,7 +341,7 @@ namespace Umbraco.Core.Models var name = GetName(culture); if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishName(culture, name); + SetPublishInfos(culture, name, DateTime.Now); } // property.PublishValue throws on invalid variation, so filter them out @@ -331,7 +364,7 @@ namespace Umbraco.Core.Models var name = GetName(culture); if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishName(culture, name); + SetPublishInfos(culture, name, DateTime.Now); // property.PublishCultureValues only deals with supported variations (if any) foreach (var property in Properties) diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index ca5f9024a7..7bc327c2d0 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -164,7 +164,7 @@ namespace Umbraco.Core.Models return; } - if ((ContentTypeBase.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) == 0) + if (!ContentTypeBase.Variations.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) throw new NotSupportedException("Content type does not support varying name by culture."); if (_names == null) diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 7d0ffea31e..952fa0cd88 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -221,7 +221,7 @@ namespace Umbraco.Core.Models { variation = ContentVariation.InvariantNeutral; } - if ((Variations & variation) == 0) + if (!Variations.Has(variation)) { if (throwIfInvalid) throw new NotSupportedException($"Variation {variation} is invalid for content type \"{Alias}\"."); diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 69e01f4963..ab995b9b04 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -89,6 +89,12 @@ namespace Umbraco.Core.Models /// whenever values for this culture are unpublished. /// bool IsCulturePublished(string culture); + + // fixme doc + DateTime GetDateCulturePublished(string culture); + + // fixme doc + bool IsCultureEdited(string culture); /// /// Gets the name of the published version of the content for a given culture. diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index b34eca7c57..66299e7749 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -243,7 +243,7 @@ namespace Umbraco.Core.Models { variation = ContentVariation.InvariantNeutral; } - if ((Variations & variation) == 0) + if (!Variations.Has(variation)) { if (throwIfInvalid) throw new NotSupportedException($"Variation {variation} is invalid for property type \"{Alias}\"."); diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 68afd4f244..79dd45b73b 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -17,10 +17,10 @@ namespace Umbraco.Core public const string NodeXml = /*TableNamePrefix*/ "cms" + "ContentXml"; public const string NodePreviewXml = /*TableNamePrefix*/ "cms" + "PreviewXml"; // fixme dbfix kill merge with ContentXml - public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; // fixme dbfixrename and split uElementType, uDocumentType + public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; // fixme dbfix rename and split uElementType, uDocumentType public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType"; - public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; // fixme dbfixmust rename corresponding DTO - public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; // fixme dbfixwhy can't we just use uNode for this? + public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; // fixme dbfix must rename corresponding DTO + public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; // fixme dbfix why can't we just use uNode for this? public const string DataType = TableNamePrefix + "DataType"; public const string Template = /*TableNamePrefix*/ "cms" + "Template"; @@ -28,6 +28,7 @@ namespace Umbraco.Core public const string ContentVersion = TableNamePrefix + "ContentVersion"; public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; public const string Document = TableNamePrefix + "Document"; + public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; public const string MediaVersion = TableNamePrefix + "MediaVersion"; diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs index 104a707669..dbba897f7a 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -25,21 +25,21 @@ namespace Umbraco.Core.Persistence.Dtos [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] public int LanguageId { get; set; } + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string Culture { get; set; } + [Column("name")] - [NullSetting(NullSetting = NullSettings.Null)] public string Name { get; set; } - [Column("available")] - public bool Available { get; set; } - - [Column("availableDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? AvailableDate { get; set; } + [Column("date")] + public DateTime Date { get; set; } + // fixme want? [Column("availableUserId")] // [ForeignKey(typeof(UserDto))] -- there is no foreign key so we can delete users without deleting associated content //[NullSetting(NullSetting = NullSettings.Null)] - public int AvailableUserId { get; set; } + public int PublishedUserId { get; set; } [Column("edited")] public bool Edited { get; set; } diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs new file mode 100644 index 0000000000..be4cf7c023 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs @@ -0,0 +1,34 @@ +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Persistence.Dtos +{ + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class DocumentCultureVariationDto + { + private const string TableName = Constants.DatabaseSchema.Tables.DocumentCultureVariation; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] + public int NodeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } + + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string Culture { get; set; } + + [Column("edited")] + public bool Edited { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 10f2918204..d25f921f1a 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -4,7 +4,6 @@ using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Factories { @@ -90,15 +89,25 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - public static IEnumerable BuildDtos(int currentVersionId, int publishedVersionId, IEnumerable properties, ILanguageRepository languageRepository, out bool edited) + public static IEnumerable BuildDtos(int currentVersionId, int publishedVersionId, IEnumerable properties, ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures) { var propertyDataDtos = new List(); edited = false; + editedCultures = null; // don't allocate unless necessary foreach (var property in properties) { if (property.PropertyType.IsPublishing) - { + { + // fixme + // why only CultureNeutral? + // then, the tree can only show when a CultureNeutral value has been modified, but not when + // a CultureSegment has been modified, so if I edit some french/mobile thing, the tree will + // NOT tell me that I have changes? + + var editingCultures = property.PropertyType.Variations.Has(ContentVariation.CultureNeutral); + if (editingCultures && editedCultures == null) editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { @@ -117,6 +126,13 @@ namespace Umbraco.Core.Persistence.Factories // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); edited |= !sameValues; + + if (editingCultures && // cultures can be edited, ie CultureNeutral is supported + propertyValue.Culture != null && propertyValue.Segment == null && // and value is CultureNeutral + !sameValues) // and edited and published are different + { + editedCultures.Add(propertyValue.Culture); // report culture as edited + } } } else diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 848019cf95..e47e8aa33e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -315,10 +315,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited); + var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); + // name also impacts 'edited' + if (content.PublishName != content.Name) + edited = true; + // persist the document dto // at that point, when publishing, the entity still has its old Published value // so we need to explicitely update the dto to persist the correct value @@ -329,12 +333,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the variations - if ((content.ContentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) + if (content.ContentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)) { - foreach (var variationDto in GetVariationDtos(content, publishing)) - Database.Insert(variationDto); + // names also impact 'edited' + foreach (var (culture, name) in content.Names) + if (name != content.GetPublishName(culture)) + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); + + // insert content variations + Database.InsertBulk(GetContentVariationDtos(content, publishing)); + + // insert document variations + Database.InsertBulk(GetDocumentVariationDtos(content, publishing, editedCultures)); } + // refresh content + if (editedCultures != null) + content.SetCultureEdited(editedCultures); + // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -454,26 +470,51 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(documentVersionDto); } - var versionToDelete = publishing ? new[] { content.VersionId, content.PublishedVersionId } : new[] { content.VersionId }; - - // replace the property data - var deletePropertyDataSql = Sql().Delete().WhereIn(x => x.VersionId, versionToDelete); + // replace the property data (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var versionToDelete = publishing ? content.PublishedVersionId : content.VersionId; + var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, LanguageRepository, out var edited); + // insert property data + var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // replace the variations - if ((content.ContentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) - { - var deleteVariations = Sql().Delete().WhereIn(x => x.VersionId, versionToDelete); - Database.Execute(deleteVariations); + // name also impacts 'edited' + if (content.PublishName != content.Name) + edited = true; - foreach (var variationDto in GetVariationDtos(content, publishing)) - Database.Insert(variationDto); + if (content.ContentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)) + { + // names also impact 'edited' + foreach (var (culture, name) in content.Names) + if (name != content.GetPublishName(culture)) + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); + + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); + + // replace the document version variations (rather than updating) + var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); + Database.Execute(deleteDocumentVariations); + + // fixme is it OK to use NPoco InsertBulk here, or should we use our own BulkInsertRecords? + // (same in PersistNewItem above) + + // insert content variations + Database.InsertBulk(GetContentVariationDtos(content, publishing)); + + // insert document variations + Database.InsertBulk(GetDocumentVariationDtos(content, publishing, editedCultures)); } + // refresh content + if (editedCultures != null) + content.SetCultureEdited(editedCultures); + // update the document dto // at that point, when un/publishing, the entity still has its old Published value // so we need to explicitely update the dto to persist the correct value @@ -853,12 +894,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // set variations, if varying - temps = temps.Where(x => x.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)).ToList(); + temps = temps.Where(x => x.ContentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)).ToList(); if (temps.Count > 0) { - var variations = GetVariations(temps); + // load all variations for all documents from database, in one query + var contentVariations = GetContentVariations(temps); + var documentVariations = GetDocumentVariations(temps); foreach (var temp in temps) - SetContentVariations(temp.Content, variations); + SetVariations(temp.Content, contentVariations, documentVariations); } return content; @@ -886,10 +929,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Properties = properties[dto.DocumentVersionDto.Id]; // set variations, if varying - if ((contentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) + if (contentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)) { - var variations = GetVariations(ltemp); - SetContentVariations(content, variations); + var contentVariations = GetContentVariations(ltemp); + var documentVariations = GetDocumentVariations(ltemp); + SetVariations(content, contentVariations, documentVariations); } // reset dirty initial properties (U4-1946) @@ -897,21 +941,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return content; } - private void SetContentVariations(Content content, IDictionary> variations) + private void SetVariations(Content content, IDictionary> contentVariations, IDictionary> documentVariations) { - if (variations.TryGetValue(content.VersionId, out var variation)) - foreach (var v in variation) - { + if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) + foreach (var v in contentVariation) content.SetName(v.Culture, v.Name); - } - if (content.PublishedVersionId > 0 && variations.TryGetValue(content.PublishedVersionId, out variation)) - foreach (var v in variation) - { - content.SetPublishName(v.Culture, v.Name); - } + if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) + foreach (var v in contentVariation) + content.SetPublishInfos(v.Culture, v.Name, v.Date); + if (documentVariations.TryGetValue(content.Id, out var documentVariation)) + foreach (var v in documentVariation.Where(x => x.Edited)) + content.SetCultureEdited(v.Culture); } - private IDictionary> GetVariations(List> temps) + private IDictionary> GetContentVariations(List> temps) where T : class, IContentBase { var versions = new List(); @@ -921,7 +964,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (temp.PublishedVersionId > 0) versions.Add(temp.PublishedVersionId); } - if (versions.Count == 0) return new Dictionary>(); + if (versions.Count == 0) return new Dictionary>(); var dtos = Database.FetchByGroups(versions, 2000, batch => Sql() @@ -929,50 +972,104 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .From() .WhereIn(x => x.VersionId, batch)); - var variations = new Dictionary>(); + var variations = new Dictionary>(); foreach (var dto in dtos) { if (!variations.TryGetValue(dto.VersionId, out var variation)) - variations[dto.VersionId] = variation = new List(); + variations[dto.VersionId] = variation = new List(); - variation.Add(new CultureVariation + variation.Add(new ContentVariation { Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Name = dto.Name, - Available = dto.Available + Date = dto.Date }); } return variations; } - private IEnumerable GetVariationDtos(IContent content, bool publishing) + private IDictionary> GetDocumentVariations(List> temps) + where T : class, IContentBase { + var ids = temps.Select(x => x.Id); + + var dtos = Database.FetchByGroups(ids, 2000, batch => + Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch)); + + var variations = new Dictionary>(); + + foreach (var dto in dtos) + { + if (!variations.TryGetValue(dto.NodeId, out var variation)) + variations[dto.NodeId] = variation = new List(); + + variation.Add(new DocumentVariation + { + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), + Edited = dto.Edited + }); + } + + return variations; + } + + private IEnumerable GetContentVariationDtos(IContent content, bool publishing) + { + // create dtos for the 'current' (non-published) version, all cultures foreach (var (culture, name) in content.Names) yield return new ContentVersionCultureVariationDto { VersionId = content.VersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Name = name + Culture = culture, + Name = name, + Date = content.UpdateDate }; + // if not publishing, we're just updating the 'current' (non-published) version, + // so there are no DTOs to create for the 'published' version which remains unchanged if (!publishing) yield break; + // create dtos for the 'published' version, for published cultures (those having a name) foreach (var (culture, name) in content.PublishNames) yield return new ContentVersionCultureVariationDto { VersionId = content.PublishedVersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Name = name + Culture = culture, + Name = name, + Date = content.GetDateCulturePublished(culture) }; } - private class CultureVariation + private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures) + { + foreach (var (culture, name) in content.Names) + yield return new DocumentCultureVariationDto + { + NodeId = content.Id, + LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), + Culture = culture, + Edited = !content.IsCulturePublished(culture) || editedCultures.Contains(culture) // if not published, always edited + }; + } + + private class ContentVariation { public string Culture { get; set; } public string Name { get; set; } - public bool Available { get; set; } + public DateTime Date { get; set; } + } + + private class DocumentVariation + { + public string Culture { get; set; } + public bool Edited { get; set; } } #region Utilities diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index dfb30a9cb3..982a5bb885 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -279,7 +279,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -336,7 +336,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 04e5d64b06..98c38603b1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -306,7 +306,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -371,7 +371,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == member.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7549d4ba2b..12631dc8fe 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -320,6 +320,7 @@ + @@ -336,7 +337,8 @@ - + + @@ -369,6 +371,7 @@ + diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 70e3d574b3..5bfd3f2de5 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -164,22 +164,26 @@ namespace Umbraco.Tests.Models const string langFr = "fr-FR"; const string langUk = "en-UK"; + // throws if the content type does not support the variation Assert.Throws(() => content.SetName(langFr, "name-fr")); + // now it will work contentType.Variations = ContentVariation.CultureNeutral; + // invariant name works content.Name = "name"; Assert.AreEqual("name", content.GetName(null)); content.SetName(null, "name2"); Assert.AreEqual("name2", content.Name); Assert.AreEqual("name2", content.GetName(null)); + // variant names work content.SetName(langFr, "name-fr"); content.SetName(langUk, "name-uk"); - Assert.AreEqual("name-fr", content.GetName(langFr)); Assert.AreEqual("name-uk", content.GetName(langUk)); + // variant dictionary of names work Assert.AreEqual(2, content.Names.Count); Assert.IsTrue(content.Names.ContainsKey(langFr)); Assert.AreEqual("name-fr", content.Names[langFr]); @@ -188,7 +192,7 @@ namespace Umbraco.Tests.Models } [Test] - public void ContentTests() + public void ContentPublishValues() { const string langFr = "fr-FR"; @@ -296,6 +300,61 @@ namespace Umbraco.Tests.Models Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); } + [Test] + public void ContentPublishVariations() + { + const string langFr = "fr-FR"; + const string langUk = "en-UK"; + const string langEs = "es-ES"; + + var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop" }; + var contentType = new ContentType(-1) { Alias = "contentType" }; + contentType.AddPropertyType(propertyType); + + var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; + + contentType.Variations |= ContentVariation.CultureNeutral; + propertyType.Variations |= ContentVariation.CultureNeutral; + + content.SetValue("prop", "a"); + content.SetValue("prop", "a-fr", langFr); + content.SetValue("prop", "a-uk", langUk); + content.SetValue("prop", "a-es", langEs); + + // cannot publish without a name + Assert.Throws(() => content.PublishValues(langFr)); + + // works with a name + // and then FR is available, and published + content.SetName(langFr, "name-fr"); + content.PublishValues(langFr); + + // now UK is available too + content.SetName(langUk, "name-uk"); + + // test available, published + Assert.IsTrue(content.IsCultureAvailable(langFr)); + Assert.IsTrue(content.IsCulturePublished(langFr)); + Assert.AreEqual("name-fr", content.GetPublishName(langFr)); + Assert.AreNotEqual(DateTime.MinValue, content.GetDateCulturePublished(langFr)); + Assert.IsFalse(content.IsCultureEdited(langFr)); // once published, edited is *wrong* until saved + + Assert.IsTrue(content.IsCultureAvailable(langUk)); + Assert.IsFalse(content.IsCulturePublished(langUk)); + Assert.IsNull(content.GetPublishName(langUk)); + Assert.Throws(() => content.GetDateCulturePublished(langUk)); // not published! + Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited + + Assert.IsFalse(content.IsCultureAvailable(langEs)); + Assert.IsFalse(content.IsCulturePublished(langEs)); + Assert.IsNull(content.GetPublishName(langEs)); + Assert.Throws(() => content.GetDateCulturePublished(langEs)); // not published! + Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited + + // cannot test IsCultureEdited here - as that requires the content service and repository + // see: ContentServiceTests.Can_SaveRead_Variations + } + [Test] public void IsDirtyTests() { diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index b3bdf4f1ba..c45d9be5f3 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2475,231 +2475,351 @@ namespace Umbraco.Tests.Services } [Test] - public void Can_SaveAndRead_Names() + public void Can_SaveRead_Variations() { var languageService = ServiceContext.LocalizationService; var langFr = new Language("fr-FR"); var langUk = new Language("en-UK"); - var langDe = new Language("de-DE"); + var langDe = new Language("de-DE"); + languageService.Save(langFr); languageService.Save(langUk); - languageService.Save(langDe); + languageService.Save(langDe); var contentTypeService = ServiceContext.ContentTypeService; - - // fixme - // contentType.Variations is InvariantNeutral | CultureNeutral - // propertyType.Variations can only be a subset of contentType.Variations - ie cannot *add* anything - // (at least, we should validate this) - // but then, - // if the contentType supports InvariantNeutral | CultureNeutral, - // the propertyType should support InvariantNeutral, or both, but not solely CultureNeutral? - // but does this mean that CultureNeutral implies InvariantNeutral? - // can a contentType *not* support InvariantNeutral? - - var contentType = contentTypeService.Get("umbTextpage"); + + // fixme + // contentType.Variations is InvariantNeutral | CultureNeutral + // propertyType.Variations can only be a subset of contentType.Variations - ie cannot *add* anything + // (at least, we should validate this) + // but then, + // if the contentType supports InvariantNeutral | CultureNeutral, + // the propertyType should support InvariantNeutral, or both, but not solely CultureNeutral? + // but does this mean that CultureNeutral implies InvariantNeutral? + // can a contentType *not* support InvariantNeutral? + + var contentType = contentTypeService.Get("umbTextpage"); contentType.Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral; contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.CultureNeutral }); - contentTypeService.Save(contentType); - + contentTypeService.Save(contentType); + var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage"); + var content = contentService.Create("Home US", - 1, "umbTextpage"); + + // act + + content.SetValue("author", "Barack Obama"); + content.SetValue("prop", "value-fr1", langFr.IsoCode); + content.SetValue("prop", "value-uk1", langUk.IsoCode); + content.SetName(langFr.IsoCode, "name-fr"); + content.SetName(langUk.IsoCode, "name-uk"); + contentService.Save(content); + + // content has been saved, + // it has names, but no publishNames, and no published cultures + + var content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.IsNull(content2.GetValue("prop", langUk.IsoCode, published: true)); + + Assert.IsNull(content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.IsNull(content2.GetPublishName(langUk.IsoCode)); + + // only fr and uk have a name, and are available + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // nothing has been published yet + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, false), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, false), (langDe, false)); + + // not published => must be edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + // act + + content.PublishValues(langFr.IsoCode); + content.PublishValues(langUk.IsoCode); + contentService.SaveAndPublish(content); + + // both FR and UK have been published, + // and content has been published, + // it has names, publishNames, and published cultures + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + + Assert.IsNull(content2.PublishName); // we haven't published InvariantNeutral + Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr and uk have been published now + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr and uk, published without changes, not edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); - // act - - content.SetValue("author", "Barack Obama"); - content.SetValue("prop", "value-fr1", langFr.IsoCode); - content.SetValue("prop", "value-uk1", langUk.IsoCode); - content.SetName(langFr.IsoCode, "name-fr"); - content.SetName(langUk.IsoCode, "name-uk"); - contentService.Save(content); + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - // content has been saved, - // it has names, but no publishNames, and no published cultures + // note that content and content2 culture published dates might be slightly different due to roundtrip to database - var content2 = contentService.GetById(content.Id); + + // act + + content.PublishValues(); + contentService.SaveAndPublish(content); + + // now it has publish name for invariant neutral + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.PublishName); - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + + // act + + content.SetName(null, "Home US2"); + content.SetName(langFr.IsoCode, "name-fr2"); + content.SetName(langUk.IsoCode, "name-uk2"); + content.SetValue("author", "Barack Obama2"); + content.SetValue("prop", "value-fr2", langFr.IsoCode); + content.SetValue("prop", "value-uk2", langUk.IsoCode); + contentService.Save(content); + + // content has been saved, + // it has updated names, unchanged publishNames, and published cultures + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("Barack Obama2", content2.GetValue("author")); + Assert.AreEqual("Barack Obama", content2.GetValue("author", published: true)); + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.IsNull(content2.GetValue("prop", langUk.IsoCode, published: true)); + // we have changed values so now fr and uk are edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - Assert.IsNull(content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.IsNull(content2.GetPublishName(langUk.IsoCode)); + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - Assert.IsTrue(content.IsCultureAvailable(langFr.IsoCode)); - Assert.IsTrue(content.IsCultureAvailable(langUk.IsoCode)); - Assert.IsFalse(content.IsCultureAvailable(langDe.IsoCode)); + + // act + // cannot just 'save' since we are changing what's published! + + content.ClearPublishedValues(langFr.IsoCode); + contentService.SaveAndPublish(content); + + // content has been published, + // the french culture is gone + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr is not published anymore + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // and so, fr has to be edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); - // act + // act - content.PublishValues(langFr.IsoCode); - content.PublishValues(langUk.IsoCode); - contentService.SaveAndPublish(content); + contentService.Unpublish(content); + + // content has been unpublished, + // but properties, names, etc. retain their 'published' values so the content + // can be re-published in its exact original state (before being unpublished) + // + // BEWARE! + // in order for a content to be unpublished as a whole, and then republished in + // its exact previous state, properties and names etc. retain their published + // values even though the content is not published - hence many things being + // non-null or true below - always check against content.Published to be sure + + content2 = contentService.GetById(content.Id); + + Assert.IsFalse(content2.Published); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); // not null, see note above + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); // not null, see note above + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); // has value, see note above + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr is not published anymore - uk still is, see note above + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // and so, fr has to be edited - uk still is + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - // both FR and UK have been published, - // and content has been published, - // it has names, publishNames, and published cultures - content2 = contentService.GetById(content.Id); + // act - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + contentService.SaveAndPublish(content); - Assert.IsNull(content2.PublishName); // we haven't published InvariantNeutral - Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + // content has been re-published, + // everything is back to what it was before being unpublished - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + content2 = contentService.GetById(content.Id); + + Assert.IsTrue(content2.Published); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // no change, back to published + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // no change, back to published + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - // act + // act - content.PublishValues(); - contentService.SaveAndPublish(content); + content.PublishValues(langUk.IsoCode); + contentService.SaveAndPublish(content); + + content2 = contentService.GetById(content.Id); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // now, uk is no more edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - // now it has publish name for invariant neutral - - content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.PublishName); + // act - // act + content.SetName(langUk.IsoCode, "name-uk3"); + contentService.Save(content); + + content2 = contentService.GetById(content.Id); - content.SetName(null, "Home US2"); - content.SetName(langFr.IsoCode, "name-fr2"); - content.SetName(langUk.IsoCode, "name-uk2"); - content.SetValue("author", "Barack Obama2"); - content.SetValue("prop", "value-fr2", langFr.IsoCode); - content.SetValue("prop", "value-uk2", langUk.IsoCode); - contentService.Save(content); - - // content has been saved, - // it has updated names, unchanged publishNames, and published cultures + // changing the name = edited! + Assert.IsTrue(content.IsCultureEdited(langUk.IsoCode)); + Assert.IsTrue(content2.IsCultureEdited(langUk.IsoCode)); + } - content2 = contentService.GetById(content.Id); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); - Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - - Assert.AreEqual("Barack Obama2", content2.GetValue("author")); - Assert.AreEqual("Barack Obama", content2.GetValue("author", published: true)); - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - - Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - - // act - // cannot just 'save' since we are changing what's published! - - content.ClearPublishedValues(langFr.IsoCode); - contentService.SaveAndPublish(content); - - // content has been published, - // the french culture is gone - - content2 = contentService.GetById(content.Id); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - - // act - - contentService.Unpublish(content); - - // content has been unpublished, - // but properties, names, etc. retain their 'published' values so the content - // can be re-published in its exact original state (before being unpublished) - // - // BEWARE! - // in order for a content to be unpublished as a whole, and then republished in - // its exact previous state, properties and names etc. retain their published - // values even though the content is not published - hence many things being - // non-null or true below - always check against content.Published to be sure - - content2 = contentService.GetById(content.Id); - - Assert.IsFalse(content2.Published); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); // not null, see note above - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); // not null, see note above - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); // has value, see note above - - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); // still true, see note above - - // act - - contentService.SaveAndPublish(content); - - // content has been re-published, - // everything is back to what it was before being unpublished - - content2 = contentService.GetById(content.Id); - - Assert.IsTrue(content2.Published); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + private void AssertPerCulture(IContent item, Func getter, params (ILanguage Language, bool Result)[] testCases) + { + foreach (var testCase in testCases) + { + var value = getter(item, testCase.Language.IsoCode); + Assert.AreEqual(testCase.Result, value, $"Expected {testCase.Result} and got {value} for culture {testCase.Language.IsoCode}."); + } } private IEnumerable CreateContentHierarchy() diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 124c77846d..8d8e127131 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -457,8 +457,8 @@ namespace Umbraco.Tests.Services ServiceContext.ContentTypeService.Save(contentType); var c1 = MockedContent.CreateSimpleContent(contentType, "Test", -1); - c1.SetName(_langFr.Id, "Test - FR"); - c1.SetName(_langEs.Id, "Test - ES"); + c1.SetName(_langFr.IsoCode, "Test - FR"); + c1.SetName(_langEs.IsoCode, "Test - ES"); ServiceContext.ContentService.Save(c1); var result = service.Get(c1.Id, UmbracoObjectTypes.Document); @@ -486,8 +486,8 @@ namespace Umbraco.Tests.Services var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); if (i % 2 == 0) { - c1.SetName(_langFr.Id, "Test " + i + " - FR"); - c1.SetName(_langEs.Id, "Test " + i + " - ES"); + c1.SetName(_langFr.IsoCode, "Test " + i + " - FR"); + c1.SetName(_langEs.IsoCode, "Test " + i + " - ES"); } ServiceContext.ContentService.Save(c1); } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 467562428d..6ce5f049e8 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -956,7 +956,8 @@ namespace Umbraco.Web.Editors //set the name according to the culture settings if (contentItem.LanguageId.HasValue && contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) { - contentItem.PersistedContent.SetName(contentItem.LanguageId, contentItem.Name); + var culture = Services.LocalizationService.GetLanguageById(contentItem.LanguageId.Value).IsoCode; + contentItem.PersistedContent.SetName(culture, contentItem.Name); } else { diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs index 125db912be..41383764bb 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs @@ -12,14 +12,8 @@ namespace Umbraco.Web.Models.Mapping { public string Resolve(IContent source, ContentItemDisplay destination, string destMember, ResolutionContext context) { - var langId = context.GetLanguageId(); - if (langId.HasValue && source.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) - { - //return the culture name being requested - return source.GetName(langId); - } - - return source.Name; + var culture = context.GetCulture(); + return source.GetName(culture); } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs index 72842c5354..1348dbb8a9 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs @@ -163,7 +163,7 @@ namespace Umbraco.Web.Models.Mapping // fixme not so clean really var isPublishing = typeof(IContentType).IsAssignableFrom(typeof(TDestination)); - return mapping + mapping = mapping //only map id if set to something higher then zero .ForMember(dest => dest.Id, opt => opt.Condition(src => (Convert.ToInt32(src.Id) > 0))) .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Convert.ToInt32(src.Id))) @@ -179,10 +179,14 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.PropertyGroups, opt => opt.Ignore()) .ForMember(dest => dest.NoGroupPropertyTypes, opt => opt.Ignore()) // ignore, composition is managed in AfterMapContentTypeSaveToEntity - .ForMember(dest => dest.ContentTypeComposition, opt => opt.Ignore()) + .ForMember(dest => dest.ContentTypeComposition, opt => opt.Ignore()); - .ForMember(dto => dto.Variations, opt => opt.ResolveUsing>()) + // ignore for members + mapping = typeof(TDestination) == typeof(IMemberType) + ? mapping.ForMember(dto => dto.Variations, opt => opt.Ignore()) + : mapping.ForMember(dto => dto.Variations, opt => opt.ResolveUsing>()); + mapping = mapping .ForMember( dest => dest.AllowedContentTypes, opt => opt.MapFrom(src => src.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)))) @@ -257,6 +261,8 @@ namespace Umbraco.Web.Models.Mapping // because all property collections were rebuilt, there is no need to remove // some old properties, they are just gone and will be cleared by the repository }); + + return mapping; } private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, IEnumerable destOrigGroups)