From ecf9a928d7a91765b68f046f542c25e51d4fafa0 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Jun 2018 14:18:57 +0200 Subject: [PATCH] Refactor variations --- .../ContentVariationExtensions.cs | 148 ++++---- .../Migrations/Install/DatabaseDataCreator.cs | 40 +-- src/Umbraco.Core/Models/Content.cs | 337 ++++++++---------- src/Umbraco.Core/Models/ContentBase.cs | 185 +++++----- src/Umbraco.Core/Models/ContentTypeBase.cs | 45 +-- src/Umbraco.Core/Models/ContentVariation.cs | 54 ++- src/Umbraco.Core/Models/IContent.cs | 104 +++--- src/Umbraco.Core/Models/IContentBase.cs | 39 +- src/Umbraco.Core/Models/IContentTypeBase.cs | 26 +- src/Umbraco.Core/Models/Property.cs | 218 ++++++++--- src/Umbraco.Core/Models/PropertyType.cs | 37 +- .../PublishedContent/PublishedContentType.cs | 2 +- .../PublishedContentTypeFactory.cs | 8 +- .../PublishedContent/RawValueProperty.cs | 2 +- .../Factories/ContentBaseFactory.cs | 25 +- .../Persistence/Factories/PropertyFactory.cs | 8 +- .../Implement/DocumentRepository.cs | 201 ++++++----- .../Implement/EntityRepository.cs | 2 +- .../Services/Implement/ContentService.cs | 2 +- src/Umbraco.Core/StringExtensions.cs | 2 +- .../Strings/DefaultUrlSegmentProvider.cs | 2 +- src/Umbraco.Tests/Models/VariationTests.cs | 197 ++++++---- .../Repositories/ContentRepositoryTest.cs | 32 +- .../PublishedContent/NuCacheTests.cs | 12 +- .../PublishedContentDataTableTests.cs | 4 +- .../PublishedContent/PublishedRouterTests.cs | 2 +- .../SolidPublishedSnapshot.cs | 4 +- .../Routing/ContentFinderByAliasTests.cs | 4 +- .../ContentFinderByAliasWithDomainsTests.cs | 4 +- src/Umbraco.Tests/Routing/UrlProviderTests.cs | 9 +- .../Routing/UrlsProviderWithDomainsTests.cs | 6 +- .../Services/ContentServiceTests.cs | 134 +++---- .../Services/EntityServiceTests.cs | 19 +- .../Testing/TestingTests/MockTests.cs | 3 +- .../UmbracoExamine/IndexInitializer.cs | 4 +- .../UmbracoExamine/SearchTests.cs | 2 +- .../Web/TemplateUtilitiesTests.cs | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 20 +- .../Mapping/ContentItemDisplayNameResolver.cs | 9 +- .../ContentItemDisplayVariationResolver.cs | 4 +- .../Mapping/ContentPropertyBasicConverter.cs | 4 +- .../Mapping/ContentTypeMapperProfile.cs | 2 +- .../Mapping/ContentTypeVariationsResolver.cs | 6 +- .../Mapping/PropertyTypeGroupResolver.cs | 2 +- .../Mapping/PropertyTypeVariationsResolver.cs | 7 +- .../PublishedCache/NuCache/Property.cs | 5 +- .../NuCache/PublishedContent.cs | 6 +- .../NuCache/PublishedSnapshotService.cs | 4 +- src/Umbraco.Web/PublishedContentExtensions.cs | 2 +- src/Umbraco.Web/Routing/DefaultUrlProvider.cs | 2 +- src/Umbraco.Web/Routing/DomainHelper.cs | 4 +- src/Umbraco.Web/Routing/PublishedRouter.cs | 2 +- .../Routing/UrlProviderExtensions.cs | 2 +- .../Trees/ContentTreeController.cs | 2 +- .../WebApi/Binders/ContentItemBinder.cs | 4 +- src/Umbraco.Web/umbraco.presentation/page.cs | 6 +- 56 files changed, 1121 insertions(+), 897 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 275cc61425..03abbcbc9e 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -1,82 +1,110 @@ -using Umbraco.Core.Models; +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core { /// - /// Provides extension methods for various enumerations. + /// Provides extension methods for content variations. /// public static class ContentVariationExtensions { - /// - /// Determines whether a variation has all flags set. - /// - public static bool Has(this ContentVariation variation, ContentVariation values) - => (variation & values) == values; + // fixme document + public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); + public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); + public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); + public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); + + public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); + public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); + public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); + public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + + public static bool VariesByNothing(this PublishedContentType contentType) => contentType.Variations.VariesByNothing(); + public static bool VariesByCulture(this PublishedContentType contentType) => contentType.Variations.VariesByCulture(); + public static bool VariesBySegment(this PublishedContentType contentType) => contentType.Variations.VariesBySegment(); + public static bool VariesByCultureAndSegment(this PublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); /// - /// Determines whether a variation has at least a flag set. + /// Determines whether a variation varies by nothing. /// - public static bool HasAny(this ContentVariation variation, ContentVariation values) - => (variation & values) != ContentVariation.Unknown; + public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; /// - /// Determines whether a variation does not support culture variations + /// Determines whether a variation varies by culture. /// - /// - /// - public static bool DoesNotSupportCulture(this ContentVariation variation) + public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; + + /// + /// Determines whether a variation varies by segment. + /// + public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; + + /// + /// Determines whether a variation varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) > 0; + + /// + /// Validates that a combination of culture and segment is valid for the variation. + /// + /// The variation. + /// The culture. + /// The segment. + /// A value indicating whether to perform exact validation. + /// A value indicating whether to support wildcards. + /// A value indicating whether to throw a when the combination is invalid. + /// True if the combination is valid; otherwise false. + /// + /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then + /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is + /// Culture, an invariant combination is ok. + /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type. + /// Both and can be "*" to indicate "all of them". + /// + /// Occurs when the combination is invalid, and is true. + public static bool ValidateVariation(this ContentVariation variation, string culture, string segment, bool exact, bool wildcards, bool throwIfInvalid) { - return !variation.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment); - } + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - /// - /// Determines whether a variation does support culture variations - /// - /// - /// - public static bool DoesSupportCulture(this ContentVariation variation) - { - return variation.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment); - } + bool Validate(bool variesBy, string value) + { + if (variesBy) + { + // varies by + // in exact mode, the value cannot be null (but it can be a wildcard) + // in !wildcards mode, the value cannot be a wildcard (but it can be null) + if ((exact && value == null) || (!wildcards && value == "*")) + return false; + } + else + { + // does not vary by value + // the value cannot have a value + // unless wildcards and it's "*" + if (value != null && (!wildcards || value != "*")) + return false; + } - /// - /// Determines whether a variation does not support invariant variations - /// - /// - /// - public static bool DoesNotSupportInvariant(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.InvariantSegment); - } + return true; + } - /// - /// Determines whether a variation does support invariant variations - /// - /// - /// - public static bool DoesSupportInvariant(this ContentVariation variation) - { - return variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.InvariantSegment); - } + if (!Validate(variation.VariesByCulture(), culture)) + { + if (throwIfInvalid) + throw new NotSupportedException($"Culture value \"{culture ?? ""}\" is invalid."); + return false; + } - /// - /// Determines whether a variation does not support segment variations - /// - /// - /// - public static bool DoesNotSupportSegment(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantSegment | ContentVariation.CultureSegment); - } + if (!Validate(variation.VariesBySegment(), segment)) + { + if (throwIfInvalid) + throw new NotSupportedException($"Segment value \"{segment ?? ""}\" is invalid."); + return false; + } - /// - /// Determines whether a variation does not support neutral variations - /// - /// - /// - public static bool DoesNotSupportNeutral(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral); + return true; } } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 8063ba9f46..7c02f7ad75 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -156,10 +156,10 @@ namespace Umbraco.Core.Migrations.Install private void CreateContentTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.Nothing }); } private void CreateUserData() @@ -212,23 +212,23 @@ namespace Umbraco.Core.Migrations.Install private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.DataTypes.DefaultMediaListView, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.DataTypes.DefaultMediaListView, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); //membership property types - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index e671b45968..d670d3a588 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Models private DateTime? _releaseDate; private DateTime? _expireDate; private Dictionary _publishInfos; - private HashSet _edited; + private HashSet _editedCultures; private static readonly Lazy Ps = new Lazy(); @@ -188,123 +188,136 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public IContentType ContentType => _contentType; + /// [IgnoreDataMember] - public DateTime? PublishDate { get; internal set; } + public DateTime? PublishDate { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public int? PublisherId { get; internal set; } + public int? PublisherId { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public ITemplate PublishTemplate { get; internal set; } + public ITemplate PublishTemplate { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public string PublishName { get; internal set; } + public string PublishName { get; internal set; } // set by persistence - // sets publish infos - // internal for repositories - // clear by clearing name - internal void SetPublishInfos(string culture, string name, DateTime date) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); + /// + [IgnoreDataMember] + public IEnumerable EditedCultures => CultureNames.Keys.Where(IsCultureEdited); - // this is the only place where we set PublishName (apart from factories etc), and we must ensure - // that we do have an invariant name, as soon as we have a variant name, else we would end up not - // being able to publish - and not being able to change the name, as PublishName is readonly. - // see also: DocumentRepository.EnsureInvariantNameValues() - which deals with Name. - // see also: U4-11286 - if (culture == null || string.IsNullOrEmpty(PublishName)) - { - PublishName = name; - PublishDate = date; - } + /// + [IgnoreDataMember] + public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); - if (culture != null) - { - // private method, assume that culture is valid + /// + public bool IsCulturePublished(string culture) + => _publishInfos != null && _publishInfos.ContainsKey(culture); - if (_publishInfos == null) - _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _publishInfos[culture] = (name, date); - } - } + /// + public bool IsCultureEdited(string culture) + => !IsCulturePublished(culture) || (_editedCultures != null && _editedCultures.Contains(culture)); /// [IgnoreDataMember] - public IReadOnlyDictionary PublishCultureNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; /// public string GetPublishName(string culture) { - if (culture == null) return PublishName; + if (culture.IsNullOrWhiteSpace()) return PublishName; + if (!ContentTypeBase.VariesByCulture()) return null; if (_publishInfos == null) return null; return _publishInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } - // clears a publish name - private void ClearPublishName(string culture) + /// + public DateTime? GetPublishDate(string culture) { - if (culture == null) - { - PublishName = null; - return; - } - - if (_publishInfos == null) return; - _publishInfos.Remove(culture); - if (_publishInfos.Count == 0) - _publishInfos = null; + if (culture.IsNullOrWhiteSpace()) return PublishDate; + if (!ContentTypeBase.VariesByCulture()) return null; + if (_publishInfos == null) return null; + return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } - // clears all publish names - private void ClearPublishNames() + // internal for repository + internal void SetPublishInfo(string culture, string name, DateTime date) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_publishInfos == null) + _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _publishInfos[culture] = (name, date); + } + + private void ClearPublishInfos() { - PublishName = null; _publishInfos = null; } - /// - public bool IsCulturePublished(string culture) - => !string.IsNullOrWhiteSpace(GetPublishName(culture)); - - /// - public DateTime GetCulturePublishDate(string culture) + private void ClearPublishInfo(string culture) { - if (_publishInfos != null && _publishInfos.TryGetValue(culture, out var infos)) - return infos.Date; - throw new InvalidOperationException($"Culture \"{culture}\" is not published."); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_publishInfos == null) return; + _publishInfos.Remove(culture); + if (_publishInfos.Count == 0) _publishInfos = null; } - /// - public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); + // fixme DOCUMENT + // sets publish name - used by persistence + //internal void UpdatePublishName(string culture, string name) + //{ + // if (string.IsNullOrWhiteSpace(name)) + // throw new ArgumentNullOrEmptyException(nameof(name)); - /// - public bool IsCultureEdited(string culture) - { - return string.IsNullOrWhiteSpace(GetPublishName(culture)) || (_edited != null && _edited.Contains(culture)); - } + // if (culture == null) // fixme that should NOT happen + // { + // PublishName = name; + // } + // else + // { + // // private method, assume that culture is valid + + // if (_publishInfos == null || !_publishInfos.TryGetValue(culture, out var pi)) + // throw new InvalidOperationException("Culture is not published."); + + // _publishInfos[culture] = (name, pi.Date); + // } + //} // sets a publish edited internal void SetCultureEdited(string culture) { - if (_edited == null) - _edited = new HashSet(StringComparer.OrdinalIgnoreCase); - _edited.Add(culture); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + if (_editedCultures == null) + _editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + _editedCultures.Add(culture); } // sets all publish edited internal void SetCultureEdited(IEnumerable cultures) { - _edited = new HashSet(cultures, StringComparer.OrdinalIgnoreCase); + if (cultures == null) + { + _editedCultures = null; + } + else + { + var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase); + _editedCultures = editedCultures.Count > 0 ? editedCultures : null; + } } - /// - public IEnumerable EditedCultures => CultureNames.Keys.Where(IsCultureEdited); - - /// - public IEnumerable AvailableCultures => CultureNames.Keys; - [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -312,128 +325,80 @@ namespace Umbraco.Core.Models public bool Blueprint { get; internal set; } /// - internal virtual bool TryPublishAllValues() + public virtual bool TryPublishValues(string culture = null, string segment = null) // fixme should it default to "*", "*" instead? { - // the values we want to publish should be valid - if (ValidateAllProperties().Any()) - return false; //fixme this should return an attempt with error results + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - 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 CultureNames) - { - if (string.IsNullOrWhiteSpace(name)) - return false; //fixme this should return an attempt with error results - - SetPublishInfos(culture, name, now); - } - - // property.PublishAllValues only deals with supported variations (if any) - foreach (var property in Properties) - property.PublishAllValues(); - - _publishedState = PublishedState.Publishing; - return true; - } - - /// - public virtual bool TryPublishValues(string culture = null, string segment = null) - { - // the variation should be supported by the content type - ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); + // the variation should be supported by the content type properties + if (!ContentType.SupportsPropertyVariation(culture, segment, true)) + throw new NotSupportedException($"Content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\" does not support \"{culture??""},{segment??""}\"."); // the values we want to publish should be valid if (ValidateProperties(culture, segment).Any()) - return false; //fixme this should return an attempt with error results + return false; // fixme - should return an attempt with error results - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - if (segment == null) + // when explicitely publishing values for a given segment, values for the segment are published, + // but that does not change which cultures are published. to publish a culture, publish 'null' or + // '*', and then we need to deal with culture names + if (segment == null || segment == "*") { - var name = GetName(culture); - if (string.IsNullOrWhiteSpace(name)) - return false; //fixme this should return an attempt with error results + // do not deal with invariant culture - it is always published - SetPublishInfos(culture, name, DateTime.Now); + if (culture == "*") // all cultures + { + foreach (var c in AvailableCultures) + { + var name = GetCultureName(c); + if (string.IsNullOrWhiteSpace(name)) + return false; + SetPublishInfo(c, name, DateTime.Now); + } + } + else if (!culture.IsNullOrWhiteSpace()) // one single culture + { + var name = GetCultureName(culture); + if (string.IsNullOrWhiteSpace(name)) + return false; + SetPublishInfo(culture, name, DateTime.Now); + } } - // property.PublishValue throws on invalid variation, so filter them out - foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(culture, segment, throwIfInvalid: false))) - property.PublishValue(culture, segment); + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in Properties) + property.PublishValues(culture, segment); _publishedState = PublishedState.Publishing; return true; } /// - internal virtual bool PublishCultureValues(string culture = null) + public virtual void ClearPublishedValues(string culture = null, string segment = null) // fixme should it default to "*", "*" instead? { - //fixme - needs API review as this is not used apart from in tests + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - // the values we want to publish should be valid - if (ValidatePropertiesForCulture(culture).Any()) - return false; + // the variation should be supported by the content type properties + if (!ContentType.SupportsPropertyVariation(culture, segment, true)) + throw new NotSupportedException($"Content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\" does not support \"{culture??""},{segment??""}\"."); - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - var name = GetName(culture); - if (string.IsNullOrWhiteSpace(name)) - throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishInfos(culture, name, DateTime.Now); + // when explicitely clearing values for a given segment, values for the segment are cleared, + // but that does not change which cultures are published. to unpublish a culture, clear 'null' or + // '*', and then we need to deal with culture names + if (segment == null || segment == "*") + { + // do not deal with invariant culture - it cannot be unpublished + // fixme - so should we throw instead? - // property.PublishCultureValues only deals with supported variations (if any) + if (culture == "*") // all cultures + ClearPublishInfos(); + else if (!culture.IsNullOrWhiteSpace()) // one single culture + ClearPublishInfo(culture); + } + + // property.PublishValues only publishes what is valid, variation-wise foreach (var property in Properties) - property.PublishCultureValues(culture); - - _publishedState = PublishedState.Publishing; - return true; - } - - /// - public virtual void ClearAllPublishedValues() - { - // property.ClearPublishedAllValues only deals with supported variations (if any) - foreach (var property in Properties) - property.ClearPublishedAllValues(); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishNames(); - - _publishedState = PublishedState.Publishing; - } - - /// - public virtual void ClearPublishedValues(string culture = null, string segment = null) - { - // the variation should be supported by the content type - ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); - - // property.ClearPublishedValue throws on invalid variation, so filter them out - foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(culture, segment, throwIfInvalid: false))) - property.ClearPublishedValue(culture, segment); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishName(culture); - - _publishedState = PublishedState.Publishing; - } - - /// - public virtual void ClearCulturePublishedValues(string culture = null) - { - // property.ClearPublishedCultureValues only deals with supported variations (if any) - foreach (var property in Properties) - property.ClearPublishedCultureValues(culture); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishName(culture); + property.ClearPublishedValues(culture, segment); _publishedState = PublishedState.Publishing; } @@ -444,6 +409,8 @@ namespace Umbraco.Core.Models return Id == other.Id && VersionId == other.VersionId; } + // fixme must ALSO refactor copyValues + /// public virtual void CopyAllValues(IContent other) { @@ -460,7 +427,7 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) foreach (var pvalue in property.Values) - if (property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) property.SetValue(null, pvalue.Culture, pvalue.Segment); // copy other properties @@ -470,7 +437,7 @@ namespace Umbraco.Core.Models var alias = otherProperty.PropertyType.Alias; foreach (var pvalue in otherProperty.Values) { - if (!otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (!otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) continue; var value = published ? pvalue.PublishedValue : pvalue.EditedValue; SetValue(alias, value, pvalue.Culture, pvalue.Segment); @@ -478,9 +445,9 @@ namespace Umbraco.Core.Models } // copy names - ClearNames(); + ClearCultureInfos(); foreach (var (culture, name) in other.CultureNames) - SetName(name, culture); + SetCultureName(name, culture); Name = other.Name; } @@ -500,7 +467,7 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) { - if (!property.PropertyType.ValidateVariation(culture, segment, false)) + if (!property.PropertyType.SupportsVariation(culture, segment)) continue; foreach (var pvalue in property.Values) @@ -512,7 +479,7 @@ namespace Umbraco.Core.Models var otherProperties = other.Properties; foreach (var otherProperty in otherProperties) { - if (!otherProperty.PropertyType.ValidateVariation(culture, segment, false)) + if (!otherProperty.PropertyType.SupportsVariation(culture, segment)) continue; var alias = otherProperty.PropertyType.Alias; @@ -520,7 +487,7 @@ namespace Umbraco.Core.Models } // copy name - SetName(other.GetName(culture), culture); + SetCultureName(other.GetCultureName(culture), culture); } /// @@ -536,7 +503,7 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) foreach (var pvalue in property.Values) - if (pvalue.Culture.InvariantEquals(culture) && property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (pvalue.Culture.InvariantEquals(culture) && property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) property.SetValue(null, pvalue.Culture, pvalue.Segment); // copy other properties @@ -546,7 +513,7 @@ namespace Umbraco.Core.Models var alias = otherProperty.PropertyType.Alias; foreach (var pvalue in otherProperty.Values) { - if (pvalue.Culture != culture || !otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (pvalue.Culture != culture || !otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) continue; var value = published ? pvalue.PublishedValue : pvalue.EditedValue; SetValue(alias, value, pvalue.Culture, pvalue.Segment); @@ -554,7 +521,7 @@ namespace Umbraco.Core.Models } // copy name - SetName(other.GetName(culture), culture); + SetCultureName(other.GetCultureName(culture), culture); } /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 04fbe269f8..283054d501 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -56,7 +56,7 @@ namespace Umbraco.Core.Models Id = 0; // no identity VersionId = 0; // no versions - SetName(name, culture); + SetCultureName(name, culture); _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); @@ -139,104 +139,101 @@ namespace Umbraco.Core.Models #region Cultures + // notes - common rules + // - setting a variant value on an invariant content type throws + // - getting a variant value on an invariant content type returns null + // - setting and getting the invariant value is always possible + // - setting a null value clears the value + + /// + public IEnumerable AvailableCultures + => _cultureInfos?.Select(x => x.Key) ?? Enumerable.Empty(); + + /// + public bool IsCultureAvailable(string culture) + => _cultureInfos != null && _cultureInfos.ContainsKey(culture); + /// [DataMember] public virtual IReadOnlyDictionary CultureNames => _cultureInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; - // sets culture infos - // internal for repositories - // clear by clearing name - internal void SetCultureInfos(string culture, string name, DateTime date) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); - - if (culture == null) - { - Name = name; - return; - } - - // private method, assume that culture is valid - - if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _cultureInfos[culture] = (name, date); - } - /// - public virtual void SetName(string name, string culture) + public virtual string GetCultureName(string culture) { - if (string.IsNullOrWhiteSpace(name)) - { - ClearName(culture); - return; - } - - if (culture == null) - { - Name = name; - return; - } - - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); - - if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _cultureInfos[culture] = (name, DateTime.Now); - OnPropertyChanged(Ps.Value.NamesSelector); - } - - /// - public virtual string GetName(string culture) - { - if (culture == null) return Name; + if (culture.IsNullOrWhiteSpace()) return Name; + if (!ContentTypeBase.VariesByCulture()) return null; if (_cultureInfos == null) return null; return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } /// - public bool IsCultureAvailable(string culture) - => !string.IsNullOrWhiteSpace(GetName(culture)); - - private void ClearName(string culture) + public DateTime? GetCultureDate(string culture) { - if (culture == null) - { - Name = null; - return; - } - - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); - - if (_cultureInfos == null) return; - if (!_cultureInfos.ContainsKey(culture)) - throw new InvalidOperationException($"Cannot unpublish culture {culture}, the document contains only cultures {string.Join(", ", _cultureInfos.Keys)}"); - - _cultureInfos.Remove(culture); - if (_cultureInfos.Count == 0) - _cultureInfos = null; + if (culture.IsNullOrWhiteSpace()) return null; + if (!ContentTypeBase.VariesByCulture()) return null; + if (_cultureInfos == null) return null; + return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } - protected virtual void ClearNames() + /// + public virtual void SetCultureName(string name, string culture) { - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); + if (ContentTypeBase.VariesByCulture()) // set on variant content type + { + if (culture.IsNullOrWhiteSpace()) // invariant is ok + { + Name = name; // may be null + } + else if (name.IsNullOrWhiteSpace()) // clear + { + ClearCultureInfo(culture); + } + else // set + { + SetCultureInfo(culture, name, DateTime.Now); + } + } + else // set on invariant content type + { + if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok + throw new NotSupportedException("Content type does not vary by culture."); + Name = name; // may be null + } + } + + protected void ClearCultureInfos() + { _cultureInfos = null; OnPropertyChanged(Ps.Value.NamesSelector); } - /// - public DateTime GetCultureDate(string culture) + protected void ClearCultureInfo(string culture) { - if (_cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) - return infos.Date; - throw new InvalidOperationException($"Culture \"{culture}\" is not available."); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_cultureInfos == null) return; + _cultureInfos.Remove(culture); + if (_cultureInfos.Count == 0) + _cultureInfos = null; + OnPropertyChanged(Ps.Value.NamesSelector); + } + + // internal for repository + internal void SetCultureInfo(string culture, string name, DateTime date) + { + if (name.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_cultureInfos == null) + _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _cultureInfos[culture.ToLowerInvariant()] = (name, date); + OnPropertyChanged(Ps.Value.NamesSelector); } #endregion @@ -307,17 +304,10 @@ namespace Umbraco.Core.Models #region Validation - internal virtual Property[] ValidateAllProperties() - { - //fixme - needs API review as this is not used apart from in tests - - return Properties.Where(x => !x.IsAllValid()).ToArray(); - } - /// public bool IsValid(string culture = null, string segment = null) { - var name = GetName(culture); + var name = GetCultureName(culture); if (name.IsNullOrWhiteSpace()) return false; return ValidateProperties(culture, segment).Length == 0; } @@ -325,25 +315,10 @@ namespace Umbraco.Core.Models /// public virtual Property[] ValidateProperties(string culture = null, string segment = null) { - return Properties.Where(x => - { - if (!culture.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportCulture()) - return false; //has a culture, this prop is only culture invariant, ignore - if (culture.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportInvariant()) - return false; //no culture, this prop is only culture variant, ignore - if (!segment.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportSegment()) - return false; //has segment, this prop is only segment neutral, ignore - if (segment.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportNeutral()) - return false; //no segment, this prop is only non segment neutral, ignore - return !x.IsValid(culture, segment); - }).ToArray(); - } - - internal virtual Property[] ValidatePropertiesForCulture(string culture = null) - { - //fixme - needs API review as this is not used apart from in tests - - return Properties.Where(x => !x.IsCultureValid(culture)).ToArray(); + return Properties.Where(x => // select properties... + x.PropertyType.SupportsVariation(culture, segment, true) && // that support the variation + !x.IsValid(culture, segment)) // and are not valid + .ToArray(); } #endregion diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index f1b61f424a..6f90b5201d 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } protected ContentTypeBase(IContentTypeBase parent) @@ -67,7 +67,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } /// @@ -201,33 +201,22 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _variations, Ps.Value.VaryBy); } - /// - /// Validates that a variation is valid for the content type. - /// - public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) + /// + public bool SupportsVariation(string culture, string segment, bool wildcards = false) { - ContentVariation variation; - if (culture != null) - { - variation = segment != null - ? ContentVariation.CultureSegment - : ContentVariation.CultureNeutral; - } - else if (segment != null) - { - variation = ContentVariation.InvariantSegment; - } - else - { - variation = ContentVariation.InvariantNeutral; - } - if (!Variations.Has(variation)) - { - if (throwIfInvalid) - throw new NotSupportedException($"Variation {variation} is invalid for content type \"{Alias}\"."); - return false; - } - return true; + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, true, wildcards, false); + } + + /// + public bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false) + { + // non-exact validation: can accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, false, true, false); } /// diff --git a/src/Umbraco.Core/Models/ContentVariation.cs b/src/Umbraco.Core/Models/ContentVariation.cs index 5b775b52b9..2759f2e075 100644 --- a/src/Umbraco.Core/Models/ContentVariation.cs +++ b/src/Umbraco.Core/Models/ContentVariation.cs @@ -3,34 +3,62 @@ namespace Umbraco.Core.Models { /// - /// Indicates the values accepted by a property. + /// Indicates how values can vary. /// + /// + /// Values can vary by nothing, or culture, or segment, or both. + /// Varying by culture implies that each culture version of a document can + /// be available or not, and published or not, individually. Varying by segment + /// is a property-level thing. + /// [Flags] public enum ContentVariation : byte { /// - /// Unknown. + /// Values do not vary. /// - Unknown = 0, + Nothing = 0, /// - /// Accepts values for the invariant culture and the neutral segment. + /// Values vary by culture. /// - InvariantNeutral = 1, + Culture = 1, /// - /// Accepts values for a specified culture and the neutral segment. + /// Values vary by segment. /// - CultureNeutral = 2, + Segment = 2, /// - /// Accepts values for the invariant culture and a specified segment. + /// Values vary by culture and segment. /// - InvariantSegment = 4, + CultureAndSegment = Culture | Segment - /// - /// Accepts values for a specified culture and a specified segment. - /// - CultureSegment = 8 + + // fixme - remove once we have a migration for DB values! + ///// + ///// Unknown. + ///// + //Unknown = 0, + + ///// + ///// Accepts values for the invariant culture and the neutral segment. + ///// + //InvariantNeutral = 1, + + ///// + ///// Accepts values for a specified culture and the neutral segment. + ///// + //CultureNeutral = 2, + + ///// + ///// Accepts values for the invariant culture and a specified segment. + ///// + //InvariantSegment = 4, + + ///// + ///// Accepts values for a specified culture and a specified segment. + ///// + //CultureSegment = 8 } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 13797425ed..929ea990e5 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -93,7 +93,7 @@ namespace Umbraco.Core.Models /// /// Gets the date a culture was published. /// - DateTime GetCulturePublishDate(string culture); + DateTime? GetPublishDate(string culture); /// /// Gets a value indicated whether a given culture is edited. @@ -121,12 +121,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get via the property. /// - IReadOnlyDictionary PublishCultureNames { get; } - - /// - /// Gets the available cultures. - /// - IEnumerable AvailableCultures { get; } + IReadOnlyDictionary PublishNames { get; } /// /// Gets the published cultures. @@ -161,54 +156,77 @@ namespace Umbraco.Core.Models /// IContent DeepCloneWithResetIdentities(); - /// - /// Publishes all values. - /// - /// A value indicating whether the values could be published. - /// - /// The document must then be published via the content service. - /// Values are not published if they are not valid. - /// - //fixme return an Attempt with some error results if it doesn't work - //fixme - needs API review as this is not used apart from in tests + ///// + ///// Publishes all values. + ///// + ///// A value indicating whether the values could be published. + ///// + ///// Fails if values cannot be published, e.g. if some values are not valid. + ///// Sets the property values for all cultures, including the invariant ones. + ///// Sets the published name for all culture that are available, thus publishing them all. + ///// The document must then be published via the content service SaveAndPublish method. + ///// + //// fixme - should return an attemps with error results + //// fixme - needs API review as this is not used apart from in tests << YES but users could use it //bool TryPublishAllValues(); + ///// + ///// Publishes the values for a specified culture and all segments. + ///// + ///// A value indicating whether the values could be published. + ///// + ///// Fails if values cannot be published, e.g. if some values are not valid. + ///// Sets the property values for the specified culture, and only the specified culture: must + ///// be invoked with a null culture to set the invariant values. + ///// Sets the published name for the specified culture, thus publishing the culture. + ///// The document must then be published via the content service SaveAndPublish method. + ///// + //// fixme - needs API review as this is not used apart from in tests << NO it is THAT one that we should use for now + //// fixme - should return an attemps with error results + //// fixme - should it publish the invariant values too? - NO that's done when SaveAndPublish (is it? don't think so) - BUT could we want to avoid it? + //bool TryPublishCultureValues(string culture); + /// - /// Publishes values. + /// Publishes values for a specific culture and segment. /// /// A value indicating whether the values could be published. /// - /// The document must then be published via the content service. - /// Values are not published if they are not valid. + /// Fails if values cannot be published, e.g. if some values are not valid. + /// Sets the property values but not the published name for the specified culture, + /// thus not explicitely publishing the culture. + /// The document must then be published via the content service SaveAndPublish method. /// - //fixme return an Attempt with some error results if it doesn't work + // fixme - should return an attemps with error results + // fixme - publishing for segments is not supported + // we don't know whether should it also publish the specified culture? + // we don't know how to publish segments but not neutral, etc + // what shall we do then? bool TryPublishValues(string culture = null, string segment = null); + ///// + ///// Clears published values. + ///// + ///// + ///// Clears the published name for all cultures, thus unpublishing all cultures. + ///// + //void ClearAllPublishedValues(); + /// - /// Publishes the culture/any values. + /// Clears published values for a specified culture and segment. /// - /// A value indicating whether the values could be published. /// - /// The document must then be published via the content service. - /// Values are not published if they are not valie. + /// Clears the property values but not the published name for the specified culture, + /// thus leaving the culture published. /// - //fixme - needs API review as this is not used apart from in tests - //bool PublishCultureValues(string culture = null); + void ClearPublishedValues(string culture = null, string segment = null); // fixme should NOT use - /// - /// Clears all published values. - /// - void ClearAllPublishedValues(); - - /// - /// Clears published values. - /// - void ClearPublishedValues(string culture = null, string segment = null); - - /// - /// Clears the culture/any published values. - /// - void ClearCulturePublishedValues(string culture = null); + ///// + ///// Clears published values for a specified culture, all segments. + ///// + ///// + ///// Clears the published name for the specified culture, thus unpublishing the culture. + ///// + //void ClearCulturePublishedValues(string culture = null); // fixme that one should be used! /// /// Copies values from another document. @@ -216,12 +234,12 @@ namespace Umbraco.Core.Models void CopyAllValues(IContent other); /// - /// Copies values from another document. + /// Copies values from another document for a specified culture and segment. /// void CopyValues(IContent other, string culture = null, string segment = null); /// - /// Copies culture/any values from another document. + /// Copies values from another document for a specified culture, all segments. /// void CopyCultureValues(IContent other, string culture = null); } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 1605c1da01..deda24cea0 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -29,45 +29,64 @@ namespace Umbraco.Core.Models int VersionId { get; } /// - /// Sets the name of the content item for a specified language. + /// Sets the name of the content item for a specified culture. /// /// - /// When is null, sets the invariant - /// language, which sets the property. + /// When is null, sets the invariant + /// culture name, which sets the property. + /// When is not null, throws if the content + /// type does not vary by culture. /// - void SetName(string value, string culture); + void SetCultureName(string value, string culture); /// /// Gets the name of the content item for a specified language. /// /// - /// When is null, gets the invariant - /// language, which is the value of the property. + /// When is null, gets the invariant + /// culture name, which is the value of the property. + /// When is not null, and the content type + /// does not vary by culture, returns null. /// - string GetName(string culture); + string GetCultureName(string culture); /// /// Gets the names of the content item. /// /// - /// Because a dictionary key cannot be null this cannot get the invariant - /// name, which must be get or set via the property. + /// Because a dictionary key cannot be null this cannot contain the invariant + /// culture name, which must be get or set via the property. /// IReadOnlyDictionary CultureNames { get; } + /// + /// Gets the available cultures. + /// + /// + /// Cannot contain the invariant culture, which is always available. + /// + IEnumerable AvailableCultures { get; } + /// /// Gets a value indicating whether a given culture is available. /// /// /// A culture becomes available whenever the content name for this culture is /// non-null, and it becomes unavailable whenever the content name is null. + /// Returns false for the invariant culture, in order to be consistent + /// with , even though the invariant culture is + /// always available. /// bool IsCultureAvailable(string culture); /// /// Gets the date a culture was created. /// - DateTime GetCultureDate(string culture); + /// + /// When is null, returns null. + /// If the specified culture is not available, returns null. + /// + DateTime? GetCultureDate(string culture); /// /// List of properties, which make up all the data available for this Content object diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index df171d5efc..ef5988344e 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -49,9 +49,31 @@ namespace Umbraco.Core.Models ContentVariation Variations { get; set; } /// - /// Validates that a variation is valid for the content type. + /// Validates that a combination of culture and segment is valid for the content type. /// - bool ValidateVariation(string culture, string segment, bool throwIfInvalid); + /// The culture. + /// The segment. + /// A value indicating whether wilcards are supported. + /// True if the combination is valid; otherwise false. + /// + /// The combination must match the content type variation exactly. For instance, if the content type varies by culture, + /// then an invariant culture would be invalid. + /// + bool SupportsVariation(string culture, string segment, bool wildcards = false); + + /// + /// Validates that a combination of culture and segment is valid for the content type properties. + /// + /// The culture. + /// The segment. + /// A value indicating whether wilcards are supported. + /// True if the combination is valid; otherwise false. + /// + /// The combination must be valid for properties of the content type. For instance, if the content type varies by culture, + /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content type is invariant, + /// then a variant culture is invalid, because no property could possibly vary by culture. + /// + bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false); /// /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index ac6a9b09f0..e1f05a4982 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -16,44 +16,83 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class Property : EntityBase { + // _values contains all property values, including the invariant-neutral value private List _values = new List(); + + // _pvalue contains the invariant-neutral property value private PropertyValue _pvalue; + + // _vvalues contains the (indexed) variant property values private Dictionary _vvalues; private static readonly Lazy Ps = new Lazy(); + /// + /// Initializes a new instance of the class. + /// protected Property() { } + /// + /// Initializes a new instance of the class. + /// public Property(PropertyType propertyType) { PropertyType = propertyType; } + /// + /// Initializes a new instance of the class. + /// public Property(int id, PropertyType propertyType) { Id = id; PropertyType = propertyType; } + /// + /// Represents a property value. + /// public class PropertyValue { private string _culture; private string _segment; + /// + /// Gets or sets the culture of the property. + /// + /// The culture is either null (invariant) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. public string Culture { get => _culture; - internal set => _culture = value?.ToLowerInvariant(); + internal set => _culture = value.IsNullOrWhiteSpace() ? null : value.ToLowerInvariant(); } + + /// + /// Gets or sets the segment of the property. + /// + /// The segment is either null (neutral) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. public string Segment { get => _segment; internal set => _segment = value?.ToLowerInvariant(); } + + /// + /// Gets or sets the edited value of the property. + /// public object EditedValue { get; internal set; } + + /// + /// Gets or sets the published value of the property. + /// public object PublishedValue { get; internal set; } + /// + /// Clones the property value. + /// public PropertyValue Clone() => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; } @@ -101,7 +140,7 @@ namespace Umbraco.Core.Models { // make sure we filter out invalid variations // make sure we leave _vvalues null if possible - _values = value.Where(x => PropertyType.ValidateVariation(x.Culture, x.Segment, false)).ToList(); + _values = value.Where(x => PropertyType.SupportsVariation(x.Culture, x.Segment)).ToList(); _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); _vvalues = _values.Count > (_pvalue == null ? 0 : 1) ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) @@ -135,7 +174,11 @@ namespace Umbraco.Core.Models /// public object GetValue(string culture = null, string segment = null, bool published = false) { - if (!PropertyType.ValidateVariation(culture, segment, false)) return null; + // ensure null or whitespace are nulls + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) return null; if (culture == null && segment == null) return GetPropertyValue(_pvalue, published); if (_vvalues == null) return null; return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out var pvalue) @@ -152,6 +195,8 @@ namespace Umbraco.Core.Models : pvalue.EditedValue; } + // fixme clear + /* // internal - must be invoked by the content item // does *not* validate the value - content item must validate first internal void PublishAllValues() @@ -161,14 +206,13 @@ namespace Umbraco.Core.Models PublishPropertyValue(_pvalue); // publish everything not invariant-neutral that is supported - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - PublishPropertyValue(pvalue); - } + if (_vvalues == null) return; + + var pvalues = _vvalues + .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) + .Select(x => x.Value); + foreach (var pvalue in pvalues) + PublishPropertyValue(pvalue); } // internal - must be invoked by the content item @@ -201,7 +245,61 @@ namespace Umbraco.Core.Models PublishPropertyValue(pvalue); } } + */ + // internal - must be invoked by the content item + // does *not* validate the value - content item must validate first + internal void PublishValues(string culture = null, string segment = null) + { + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + PublishPropertyValue(_pvalue); + + // then deal with everything that varies + if (_vvalues == null) return; + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches + .Select(x => x.Value); + + foreach (var pvalue in pvalues) + PublishPropertyValue(pvalue); + } + + // internal - must be invoked by the content item + internal void ClearPublishedValues(string culture = null, string segment = null) + { + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + ClearPublishedPropertyValue(_pvalue); + + // then deal with everything that varies + if (_vvalues == null) return; + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches + .Select(x => x.Value); + + foreach (var pvalue in pvalues) + ClearPublishedPropertyValue(pvalue); + } + + // fixme clear + /* // internal - must be invoked by the content item internal void ClearPublishedAllValues() { @@ -228,7 +326,7 @@ namespace Umbraco.Core.Models } // internal - must be invoked by the content item - internal void ClearPublishedCultureValues(string culture = null) + internal void ClearCulturePublishedValues(string culture = null) { if (culture == null && PropertyType.ValidateVariation(null, null, false)) ClearPublishedPropertyValue(_pvalue); @@ -243,6 +341,7 @@ namespace Umbraco.Core.Models ClearPublishedPropertyValue(pvalue); } } + */ private void PublishPropertyValue(PropertyValue pvalue) { @@ -271,7 +370,12 @@ namespace Umbraco.Core.Models /// public void SetValue(object value, string culture = null, string segment = null) { - PropertyType.ValidateVariation(culture, segment, true); + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) + throw new NotSupportedException($"Variation \"{culture??""},{segment??""}\" is not supported by the property type."); + var (pvalue, change) = GetPValue(culture, segment, true); var origValue = pvalue.EditedValue; @@ -335,65 +439,69 @@ namespace Umbraco.Core.Models /// Gets a value indicating whether all properties are valid. /// /// - internal bool IsAllValid() + internal bool IsValid(string culture = null, string segment = null) { - //fixme - needs API review as this is not used apart from in tests + // if validating invariant/neutral, and it is supported, validate + // (including ensuring that the value exists, if mandatory) + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + if (!IsValidValue(_pvalue?.EditedValue)) + return false; - // invariant-neutral is supported, validate invariant-neutral - // includes mandatory validation - if (PropertyType.ValidateVariation(null, null, false) && !IsValidValue(_pvalue)) return false; + // if validating only invariant/neutral, we are good + if (culture == null && segment == null) return true; - // either invariant-neutral is not supported, or it is valid // for anything else, validate the existing values (including mandatory), // but we cannot validate mandatory globally (we don't know the possible cultures and segments) - if (_vvalues == null) return true; + if (_vvalues == null) return culture == "*" || segment == "*" || IsValidValue(null); - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value) - .ToArray(); + .ToList(); - return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); + return pvalues.Count == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); } - /// - /// Gets a value indicating whether the culture/any values are valid. - /// - /// An invalid value can be saved, but only valid values can be published. - internal bool IsCultureValid(string culture) - { - //fixme - needs API review as this is not used apart from in tests + // fixme clear + ///// + ///// Gets a value indicating whether the culture/any values are valid. + ///// + ///// An invalid value can be saved, but only valid values can be published. + //internal bool IsCultureValid(string culture) + //{ - // culture-neutral is supported, validate culture-neutral - // includes mandatory validation - if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture))) - return false; + // // culture-neutral is supported, validate culture-neutral + // // includes mandatory validation + // if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture))) + // return false; - // either culture-neutral is not supported, or it is valid - // for anything non-neutral, validate the existing values (including mandatory), - // but we cannot validate mandatory globally (we don't know the possible segments) + // // either culture-neutral is not supported, or it is valid + // // for anything non-neutral, validate the existing values (including mandatory), + // // but we cannot validate mandatory globally (we don't know the possible segments) - if (_vvalues == null) return true; + // if (_vvalues == null) return true; - var pvalues = _vvalues - .Where(x => x.Value.Culture.InvariantEquals(culture)) - .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - .Select(x => x.Value) - .ToArray(); + // var pvalues = _vvalues + // .Where(x => x.Value.Culture.InvariantEquals(culture)) + // .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) + // .Select(x => x.Value) + // .ToArray(); - return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); - } + // return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); + //} - /// - /// Gets a value indicating whether the value is valid. - /// - /// An invalid value can be saved, but only valid values can be published. - public bool IsValid(string culture = null, string segment = null) - { - // single value -> validates mandatory - return IsValidValue(GetValue(culture, segment)); - } + ///// + ///// Gets a value indicating whether the value is valid. + ///// + ///// An invalid value can be saved, but only valid values can be published. + //public bool IsValid(string culture = null, string segment = null) + //{ + // // single value -> validates mandatory + // return IsValidValue(GetValue(culture, segment)); + //} /// /// Boolean indicating whether the passed in value is valid diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 587af74aa2..3d5fac2077 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.Models _propertyEditorAlias = dataType.EditorAlias; _valueStorageType = dataType.DatabaseType; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } /// @@ -84,7 +84,7 @@ namespace Umbraco.Core.Models _valueStorageType = valueStorageType; _forceValueStorageType = forceValueStorageType; _alias = propertyTypeAlias == null ? null : SanitizeAlias(propertyTypeAlias); - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors()); @@ -224,32 +224,17 @@ namespace Umbraco.Core.Models } /// - /// Validates that a variation is valid for the property type. + /// Determines whether the property type supports a combination of culture and segment. /// - public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) + /// The culture. + /// The segment. + /// A value indicating whether wildcards are valid. + public bool SupportsVariation(string culture, string segment, bool wildcards = false) { - ContentVariation variation; - if (culture != null) - { - variation = segment != null - ? ContentVariation.CultureSegment - : ContentVariation.CultureNeutral; - } - else if (segment != null) - { - variation = ContentVariation.InvariantSegment; - } - else - { - variation = ContentVariation.InvariantNeutral; - } - if (!Variations.Has(variation)) - { - if (throwIfInvalid) - throw new NotSupportedException($"Variation {variation} is invalid for property type \"{Alias}\"."); - return false; - } - return true; + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, true, wildcards, false); } /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index bc403b904d..e611ded6c8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -81,7 +81,7 @@ namespace Umbraco.Core.Models.PublishedContent foreach ((var alias, (var dataTypeId, var editorAlias)) in BuiltinMemberProperties) { if (aliases.Contains(alias)) continue; - propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.InvariantNeutral)); + propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 3ba908b9bf..5de5842eda 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -32,13 +32,13 @@ namespace Umbraco.Core.Models.PublishedContent } // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing) { return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations); } // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing) { return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations); } @@ -50,13 +50,13 @@ namespace Umbraco.Core.Models.PublishedContent } /// - public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.InvariantNeutral) + public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); } // for tests - internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); } diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index 5dc4a280e6..7469222ab0 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core.Models.PublishedContent public RawValueProperty(PublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { - if (propertyType.Variations != ContentVariation.InvariantNeutral) + if (propertyType.Variations != ContentVariation.Nothing) throw new ArgumentException("Property types with variations are not supported here.", nameof(propertyType)); _sourceValue = sourceValue; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index c3b4b0d24c..ec364c7c6a 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -159,8 +159,7 @@ namespace Umbraco.Core.Persistence.Factories /// public static DocumentDto BuildDto(IContent entity, Guid objectType) { - var contentBase = (Content) entity; - var contentDto = BuildContentDto(contentBase, objectType); + var contentDto = BuildContentDto(entity, objectType); var dto = new DocumentDto { @@ -170,7 +169,7 @@ namespace Umbraco.Core.Persistence.Factories ExpiresDate = entity.ExpireDate, ContentDto = contentDto, - DocumentVersionDto = BuildDocumentVersionDto(contentBase, contentDto) + DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) }; return dto; @@ -181,14 +180,13 @@ namespace Umbraco.Core.Persistence.Factories /// public static MediaDto BuildDto(IMedia entity) { - var contentBase = (Models.Media) entity; - var contentDto = BuildContentDto(contentBase, Constants.ObjectTypes.Media); + var contentDto = BuildContentDto(entity, Constants.ObjectTypes.Media); var dto = new MediaDto { NodeId = entity.Id, ContentDto = contentDto, - MediaVersionDto = BuildMediaVersionDto(contentBase, contentDto) + MediaVersionDto = BuildMediaVersionDto(entity, contentDto) }; return dto; @@ -199,8 +197,7 @@ namespace Umbraco.Core.Persistence.Factories /// public static MemberDto BuildDto(IMember entity) { - var member = (Member) entity; - var contentDto = BuildContentDto(member, Constants.ObjectTypes.Member); + var contentDto = BuildContentDto(entity, Constants.ObjectTypes.Member); var dto = new MemberDto { @@ -210,12 +207,12 @@ namespace Umbraco.Core.Persistence.Factories Password = entity.RawPasswordValue, ContentDto = contentDto, - ContentVersionDto = BuildContentVersionDto(member, contentDto) + ContentVersionDto = BuildContentVersionDto(entity, contentDto) }; return dto; } - private static ContentDto BuildContentDto(ContentBase entity, Guid objectType) + private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) { var dto = new ContentDto { @@ -228,7 +225,7 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - private static NodeDto BuildNodeDto(ContentBase entity, Guid objectType) + private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) { var dto = new NodeDto { @@ -250,7 +247,7 @@ namespace Umbraco.Core.Persistence.Factories // always build the current / VersionPk dto // we're never going to build / save old versions (which are immutable) - private static ContentVersionDto BuildContentVersionDto(ContentBase entity, ContentDto contentDto) + private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) { var dto = new ContentVersionDto { @@ -269,7 +266,7 @@ namespace Umbraco.Core.Persistence.Factories // always build the current / VersionPk dto // we're never going to build / save old versions (which are immutable) - private static DocumentVersionDto BuildDocumentVersionDto(Content entity, ContentDto contentDto) + private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) { var dto = new DocumentVersionDto { @@ -283,7 +280,7 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - private static MediaVersionDto BuildMediaVersionDto(Models.Media entity, ContentDto contentDto) + private static MediaVersionDto BuildMediaVersionDto(IMedia entity, ContentDto contentDto) { // try to get a path from the string being stored for media // fixme - only considering umbracoFile ?! diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index b0e3e2dc7d..3bea84e619 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -99,13 +99,7 @@ namespace Umbraco.Core.Persistence.Factories { 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); + var editingCultures = property.PropertyType.VariesByCulture(); if (editingCultures && editedCultures == null) editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); // publishing = deal with edit and published values diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index f2a91c11a1..202f51729a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -232,7 +232,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IContent entity) { - //fixme - stop doing this just so we have access to AddingEntity + // fixme - stop doing this - sort out IContent vs Content + // however, it's not just so we have access to AddingEntity + // there are tons of things at the end of the methods, that can only work with a true Content + // and basically, the repository requires a Content, not an IContent var content = (Content) entity; content.AddingEntity(); @@ -242,11 +245,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.Template == null) entity.Template = entity.ContentType.DefaultTemplate; - // sanitize names: ensure we have an invariant name, and names are unique-ish - // (well, only invariant name is unique at the moment) - EnsureUniqueVariationNames(entity); - EnsureInvariantNameValues(entity, publishing); - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + // sanitize names + SanitizeNames(content, publishing); // ensure that strings don't contain characters that are invalid in xml // fixme - do we really want to keep doing this here? @@ -295,7 +295,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = !publishing; - contentVersionDto.Text = publishing ? content.PublishName : content.Name; + contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -312,7 +312,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.PublishedVersionId = content.VersionId; contentVersionDto.Id = 0; contentVersionDto.Current = true; - contentVersionDto.Text = content.PublishName; + contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -326,7 +326,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // name also impacts 'edited' + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' if (content.PublishName != content.Name) edited = true; @@ -340,7 +341,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the variations - if (content.ContentType.Variations.DoesSupportCulture()) + if (content.ContentType.VariesByCulture()) { // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) @@ -355,8 +356,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // refresh content - if (editedCultures != null) - content.SetCultureEdited(editedCultures); + content.SetCultureEdited(editedCultures); // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -369,6 +369,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = true; content.PublishTemplate = content.Template; content.PublisherId = content.WriterId; + content.PublishName = content.Name; content.PublishDate = content.UpdateDate; SetEntityTags(entity, _tagRepository); @@ -378,6 +379,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = false; content.PublishTemplate = null; content.PublisherId = null; + content.PublishName = null; content.PublishDate = null; ClearEntityTags(entity, _tagRepository); @@ -400,6 +402,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { + // fixme - stop doing this - sort out IContent vs Content + // however, it's not just so we have access to AddingEntity + // there are tons of things at the end of the methods, that can only work with a true Content + // and basically, the repository requires a Content, not an IContent var content = (Content) entity; // check if we need to make any database changes at all @@ -423,11 +429,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionId)); } - // sanitize names: ensure we have an invariant name, and names are unique-ish - // (well, only invariant name is unique at the moment) - EnsureUniqueVariationNames(entity); - EnsureInvariantNameValues(entity, publishing); - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + // sanitize names + SanitizeNames(content, publishing); // ensure that strings don't contain characters that are invalid in xml // fixme - do we really want to keep doing this here? @@ -460,7 +463,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { documentVersionDto.Published = true; // now published contentVersionDto.Current = false; // no more current - contentVersionDto.Text = content.PublishName; + contentVersionDto.Text = content.Name; } Database.Update(contentVersionDto); Database.Update(documentVersionDto); @@ -491,11 +494,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // name also impacts 'edited' - if (content.PublishName != content.Name) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && content.PublishName != content.Name) edited = true; - if (content.ContentType.Variations.DoesSupportCulture()) + if (content.ContentType.VariesByCulture()) { // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) @@ -511,7 +515,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); Database.Execute(deleteDocumentVariations); - // fixme is we'd like to use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) but by using SQL Server and updating a variants name will cause: Unable to cast object of type 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // fixme is we'd like to use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. // (same in PersistNewItem above) // insert content variations @@ -522,8 +528,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // refresh content - if (editedCultures != null) - content.SetCultureEdited(editedCultures); + content.SetCultureEdited(editedCultures); // update the document dto // at that point, when un/publishing, the entity still has its old Published value @@ -550,6 +555,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = true; content.PublishTemplate = content.Template; content.PublisherId = content.WriterId; + content.PublishName = content.Name; content.PublishDate = content.UpdateDate; SetEntityTags(entity, _tagRepository); @@ -559,6 +565,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = false; content.PublishTemplate = null; content.PublisherId = null; + content.PublishName = null; content.PublishDate = null; ClearEntityTags(entity, _tagRepository); @@ -904,7 +911,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // set variations, if varying - temps = temps.Where(x => x.ContentType.Variations.DoesSupportCulture()).ToList(); + temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList(); if (temps.Count > 0) { // load all variations for all documents from database, in one query @@ -939,7 +946,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Properties = properties[dto.DocumentVersionDto.Id]; // set variations, if varying - if (contentType.Variations.DoesSupportCulture()) + if (contentType.VariesByCulture()) { var contentVariations = GetContentVariations(ltemp); var documentVariations = GetDocumentVariations(ltemp); @@ -955,10 +962,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) foreach (var v in contentVariation) - content.SetCultureInfos(v.Culture, v.Name, v.Date); + content.SetCultureInfo(v.Culture, v.Name, v.Date); if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) foreach (var v in contentVariation) - content.SetPublishInfos(v.Culture, v.Name, v.Date); + content.SetPublishInfo(v.Culture, v.Name, v.Date); if (documentVariations.TryGetValue(content.Id, out var documentVariation)) foreach (var v in documentVariation.Where(x => x.Edited)) content.SetCultureEdited(v.Culture); @@ -1038,7 +1045,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetCultureDate(culture) + Date = content.GetCultureDate(culture) ?? DateTime.MinValue // we *know* there is a value }; // if not publishing, we're just updating the 'current' (non-published) version, @@ -1046,14 +1053,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (!publishing) yield break; // create dtos for the 'published' version, for published cultures (those having a name) - foreach (var (culture, name) in content.PublishCultureNames) + 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."), Culture = culture, Name = name, - Date = content.GetCulturePublishDate(culture) + Date = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value }; } @@ -1084,90 +1091,124 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Utilities - /// - /// Ensures that the Name/PublishName properties are filled in and validates if all names are null - /// - private void EnsureInvariantNameValues(IContent content, bool publishing) + private void SanitizeNames(Content content, bool publishing) { - // here we have to ensure we have names and publish names, and to try and fix the situation if we have no name, see also: U4-11286 + // a content item *must* have an invariant name, and invariant published name + // else we just cannot write the invariant rows (node, content version...) to the database - // invariant content must have an invariant name - if (content.ContentType.Variations.DoesNotSupportCulture() && string.IsNullOrWhiteSpace(content.Name)) - throw new InvalidOperationException("Cannot save content with an empty name."); + // ensure that we have an invariant name + // invariant content = must be there already, else throw + // variant content = update with default culture or anything really + EnsureInvariantNameExists(content); - // variant content must have at least one variant name - if (content.ContentType.Variations.DoesSupportCulture()) + // ensure that that invariant name is unique + EnsureInvariantNameIsUnique(content); + + // now that we have an invariant name, which is unique, + // if publishing, ensure that we have an invariant publish name + // invariant content = must be there, else throw - then must follow the invariant name + // variant content = update with invariant name + // fixme wtf is this we never needed it, PublishName derives from Name when publishing! + //if (publishing) EnsureInvariantPublishName(content); + + // and finally, + // ensure that each culture has a unique node name + // no published name = not published + // else, it needs to be unique + EnsureVariantNamesAreUnique(content, publishing); + } + + private void EnsureInvariantNameExists(Content content) + { + if (content.ContentType.VariesByCulture()) { + // content varies by culture + // then it must have at least a variant name, else it makes no sense if (content.CultureNames.Count == 0) throw new InvalidOperationException("Cannot save content with an empty name."); - // cannot save with an empty invariant name, - // if invariant name is missing, derive it from variant names - // fixme should we always sync the invariant name with the default culture name when updating? - if (string.IsNullOrWhiteSpace(content.Name)) - { - var defaultCulture = LanguageRepository.GetDefaultIsoCode(); - if (defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName)) - content.Name = cultureName; - else - content.Name = content.CultureNames.First().Value; // only option is to take the first - } + // and then, we need to set the invariant name implicitely, + // using the default culture if it has a name, otherwise anything we can + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + content.Name = defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName) + ? cultureName + : content.CultureNames.First().Value; } - - // cannot publish without an invariant name - if (publishing && string.IsNullOrWhiteSpace(content.PublishName)) + else { - // no variant name = error - if (content.PublishCultureNames.Count == 0) - throw new InvalidOperationException("Cannot publish content with an empty name."); - - // else... we cannot deal with it here because PublishName is readonly, so in reality, PublishName - // should not be null because it should have been set when preparing the content for publish. - // see also: Content.SetPublishInfos() - it deals with PublishName + // content is invariant, and invariant content must have an explicit invariant name + if (string.IsNullOrWhiteSpace(content.Name)) + throw new InvalidOperationException("Cannot save content with an empty name."); } } + private void EnsureInvariantNameIsUnique(Content content) + { + content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); + } + + //private void EnsureInvariantPublishName(Content content) + //{ + // if (content.ContentType.VariesByCulture()) + // { + // // content varies by culture, reuse name as publish name + // content.UpdatePublishName(null, content.Name); + // } + // else + // { + // // content is invariant, and invariant content must have an explicit invariant name + // if (string.IsNullOrWhiteSpace(content.PublishName)) + // throw new InvalidOperationException("Cannot save content with an empty name."); + // // and then, must follow the name itself + // if (content.PublishName != content.Name) + // content.UpdatePublishName(null, content.Name); + // } + //} + protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); } - private SqlTemplate SqlEnsureUniqueVariationNames => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureUniqueVariationNames", tsql => tsql + private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql .Select(x => x.Id, x => x.Name, x => x.LanguageId) .From() - .InnerJoin() - .On(x => x.Id, x => x.VersionId) - .InnerJoin() - .On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.Current == SqlTemplate.Arg("current")) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId") && x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureUniqueVariationNames(IContent content) + private void EnsureVariantNamesAreUnique(Content content, bool publishing) { - if (!EnsureUniqueNaming || content.CultureNames.Count == 0) return; + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureNames.Count == 0) return; - //Get all culture names at the same level - - var sql = SqlEnsureUniqueVariationNames.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); + // get names per culture, at same level (ie all siblings) + var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); var names = Database.Fetch(sql) .GroupBy(x => x.LanguageId) .ToDictionary(x => x.Key, x => x); if (names.Count == 0) return; - foreach(var n in content.CultureNames) + foreach(var (culture, name) in content.CultureNames) { - var langId = LanguageRepository.GetIdByIsoCode(n.Key); + var langId = LanguageRepository.GetIdByIsoCode(culture); if (!langId.HasValue) continue; - if (names.TryGetValue(langId.Value, out var cultureNames)) - { - var otherLangNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherLangNames, 0, n.Value); - content.SetName(uniqueName, n.Key); - } + if (!names.TryGetValue(langId.Value, out var cultureNames)) continue; + + // get a unique name + var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name); + + if (uniqueName == content.GetCultureName(culture)) continue; + + // update the name, and the publish name if published + content.SetCultureName(uniqueName, culture); + if (publishing && content.PublishNames.ContainsKey(culture)) // fixme but what about those cultures we are NOT publishing NOW?! they shouldn't change their name! + content.SetPublishInfo(culture, uniqueName, DateTime.Now); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 75dbeca559..340eecb538 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -905,7 +905,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.Edited = dto.Edited; entity.Published = dto.Published; - if (dto.Variations.Has(ContentVariation.CultureNeutral) && dto.VariationInfo != null && dto.VariationInfo.Count > 0) + if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) { var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var info in dto.VariationInfo) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2e3fcf8522..4051a26f7f 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1056,7 +1056,7 @@ namespace Umbraco.Core.Services.Implement // not varying, or invariant culture // simply unpublish the document - if (!content.ContentType.Variations.DoesSupportCulture() || culture.IsNullOrWhiteSpace()) + if (!content.ContentType.VariesByCulture() || culture.IsNullOrWhiteSpace()) { var unpublished = UnpublishScoped(scope, content, evtMsgs, userId); if (unpublished.Success) scope.Complete(); diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index a2f5727ae4..0799c2c9a8 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -1504,7 +1504,7 @@ namespace Umbraco.Core /// /// Turns an null-or-whitespace string into a null string. /// - public static string NullEmpty(this string text) + public static string NullOrWhiteSpaceAsNull(this string text) => string.IsNullOrWhiteSpace(text) ? null : text; } } diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 09feeff338..9472ff4823 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Strings if (content.HasProperty(Constants.Conventions.Content.UrlName)) source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(source)) - source = content.GetName(culture); + source = content.GetCultureName(culture); return source; } } diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 044f8fa0cd..861dee1eca 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -2,12 +2,11 @@ using LightInject; using Moq; using NUnit.Framework; -using Umbraco.Core.Cache; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; @@ -59,6 +58,55 @@ namespace Umbraco.Tests.Models }); } + [Test] + public void ValidateVariationTests() + { + void Assert4A(ContentVariation v, string c, string s, bool xx) + { + Assert4B(v, c, s, xx, xx, xx, xx); + } + + void Assert4B(ContentVariation v, string c, string s, bool ew, bool nn, bool en, bool nw) + { + Assert.AreEqual(ew, v.ValidateVariation(c, s, true, true, false)); + Assert.AreEqual(nn, v.ValidateVariation(c, s, false, false, false)); + Assert.AreEqual(en, v.ValidateVariation(c, s, true, false, false)); + Assert.AreEqual(nw, v.ValidateVariation(c, s, false, true, false)); + } + + // always support invariant,neutral + Assert4A(ContentVariation.Nothing, null, null, true); + + // never support culture and/or segment + Assert4A(ContentVariation.Nothing, "culture", null, false); + Assert4A(ContentVariation.Nothing, null, "segment", false); + Assert4A(ContentVariation.Nothing, "culture", "segment", false); + + // support '*' only when wildcards are supported + Assert4B(ContentVariation.Nothing, "*", null, true, false, false, true); + Assert4B(ContentVariation.Nothing, null, "*", true, false, false, true); + Assert4B(ContentVariation.Nothing, "*", "*", true, false, false, true); + + + // support invariant if not exact + Assert4B(ContentVariation.Culture, null, null, false, true, false, true); + + // support invariant if not exact, '*' when wildcards are supported + Assert4B(ContentVariation.Culture, "*", null, true, false, false, true); + Assert4B(ContentVariation.Culture, null, "*", false, false, false, true); + Assert4B(ContentVariation.Culture, "*", "*", true, false, false, true); + + // never support segment + Assert4A(ContentVariation.Culture, null, "segment", false); + Assert4A(ContentVariation.Culture, "culture", "segment", false); + Assert4A(ContentVariation.Culture, "*", "segment", false); + + Assert4B(ContentVariation.Culture, null, "*", false, false, false, true); + Assert4B(ContentVariation.Culture, "culture", "*", true, false, false, true); + + // could do the same with .Segment, and .CultureAndSegment + } + [Test] public void PropertyTests() { @@ -75,7 +123,7 @@ namespace Umbraco.Tests.Models Assert.AreEqual("a", prop.GetValue(published: true)); // illegal, 'cos non-publishing - Assert.Throws(() => prop.PublishValue()); + Assert.Throws(() => prop.PublishValues()); // change propertyType.IsPublishing = true; @@ -91,7 +139,7 @@ namespace Umbraco.Tests.Models // can publish value // and get edited and published values - prop.PublishValue(); + prop.PublishValues(); Assert.AreEqual("a", prop.GetValue()); Assert.AreEqual("a", prop.GetValue(published: true)); @@ -102,57 +150,57 @@ namespace Umbraco.Tests.Models Assert.AreEqual("a", prop.GetValue(published: true)); // can clear value - prop.ClearPublishedValue(); + prop.ClearPublishedValues(); Assert.AreEqual("b", prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); - // change - propertyType.Variations |= ContentVariation.CultureNeutral; + // change - now we vary by culture + propertyType.Variations |= ContentVariation.Culture; // can set value // and get values prop.SetValue("c", langFr); - Assert.AreEqual("b", prop.GetValue()); + Assert.IsNull(prop.GetValue()); // there is no invariant value anymore Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.IsNull(prop.GetValue(langFr, published: true)); // can publish value // and get edited and published values - prop.PublishValue(langFr); - Assert.AreEqual("b", prop.GetValue()); + prop.PublishValues(langFr); + Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.AreEqual("c", prop.GetValue(langFr, published: true)); // can clear all - prop.ClearPublishedAllValues(); - Assert.AreEqual("b", prop.GetValue()); + prop.ClearPublishedValues("*"); + Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.IsNull(prop.GetValue(langFr, published: true)); // can publish all - prop.PublishAllValues(); - Assert.AreEqual("b", prop.GetValue()); - Assert.AreEqual("b", prop.GetValue(published: true)); + prop.PublishValues("*"); + Assert.IsNull(prop.GetValue()); + Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.AreEqual("c", prop.GetValue(langFr, published: true)); // same for culture - prop.ClearPublishedCultureValues(langFr); + prop.ClearPublishedValues(langFr); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.IsNull(prop.GetValue(langFr, published: true)); - prop.PublishCultureValues(langFr); + prop.PublishValues(langFr); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.AreEqual("c", prop.GetValue(langFr, published: true)); - prop.ClearPublishedCultureValues(); - Assert.AreEqual("b", prop.GetValue()); + prop.ClearPublishedValues(); // does not throw, internal, content item throws + Assert.IsNull(prop.GetValue()); + Assert.IsNull(prop.GetValue(published: true)); + prop.PublishValues(); // does not throw, internal, content item throws + Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); - prop.PublishCultureValues(); - Assert.AreEqual("b", prop.GetValue()); - Assert.AreEqual("b", prop.GetValue(published: true)); } [Test] @@ -165,23 +213,23 @@ namespace Umbraco.Tests.Models const string langUk = "en-UK"; // throws if the content type does not support the variation - Assert.Throws(() => content.SetName("name-fr", langFr)); + Assert.Throws(() => content.SetCultureName("name-fr", langFr)); // now it will work - contentType.Variations = ContentVariation.CultureNeutral; + contentType.Variations = ContentVariation.Culture; // invariant name works content.Name = "name"; - Assert.AreEqual("name", content.GetName(null)); - content.SetName("name2", null); + Assert.AreEqual("name", content.GetCultureName(null)); + content.SetCultureName("name2", null); Assert.AreEqual("name2", content.Name); - Assert.AreEqual("name2", content.GetName(null)); + Assert.AreEqual("name2", content.GetCultureName(null)); // variant names work - content.SetName("name-fr", langFr); - content.SetName("name-uk", langUk); - Assert.AreEqual("name-fr", content.GetName(langFr)); - Assert.AreEqual("name-uk", content.GetName(langUk)); + content.SetCultureName("name-fr", langFr); + content.SetCultureName("name-uk", langUk); + Assert.AreEqual("name-fr", content.GetCultureName(langFr)); + Assert.AreEqual("name-uk", content.GetCultureName(langUk)); // variant dictionary of names work Assert.AreEqual(2, content.CultureNames.Count); @@ -230,14 +278,14 @@ namespace Umbraco.Tests.Models Assert.AreEqual("b", content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - // change - contentType.Variations |= ContentVariation.CultureNeutral; - propertyType.Variations |= ContentVariation.CultureNeutral; + // change - now we vary by culture + contentType.Variations |= ContentVariation.Culture; + propertyType.Variations |= ContentVariation.Culture; // can set value // and get values content.SetValue("prop", "c", langFr); - Assert.AreEqual("b", content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop")); // there is no invariant value anymore Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); @@ -245,57 +293,57 @@ namespace Umbraco.Tests.Models // can publish value // and get edited and published values Assert.IsFalse(content.TryPublishValues(langFr)); // no name - content.SetName("name-fr", langFr); + content.SetCultureName("name-fr", langFr); Assert.IsTrue(content.TryPublishValues(langFr)); - Assert.AreEqual("b", content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // can clear all - content.ClearAllPublishedValues(); - Assert.AreEqual("b", content.GetValue("prop")); + content.ClearPublishedValues("*"); + Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); // can publish all - Assert.IsTrue(content.TryPublishAllValues()); - Assert.AreEqual("b", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); + Assert.IsTrue(content.TryPublishValues("*")); + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // same for culture - content.ClearCulturePublishedValues(langFr); + content.ClearPublishedValues(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); - content.PublishCultureValues(langFr); + content.TryPublishValues(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); - content.ClearCulturePublishedValues(); - Assert.AreEqual("b", content.GetValue("prop")); + content.ClearPublishedValues(); // clears invariant props if any + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); + content.TryPublishValues(); // publishes invariant props if any + Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - content.PublishCultureValues(); - Assert.AreEqual("b", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); var other = new Content("other", -1, contentType) { Id = 2, VersionId = 1 }; - other.SetValue("prop", "o"); + Assert.Throws(() => other.SetValue("prop", "o")); // don't even try other.SetValue("prop", "o1", langFr); // can copy other's edited value content.CopyAllValues(other); - Assert.AreEqual("o", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("o1", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // can copy self's published value content.CopyAllValues(content); - Assert.AreEqual("b", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); } @@ -305,29 +353,31 @@ namespace Umbraco.Tests.Models { const string langFr = "fr-FR"; - var contentType = new ContentType(-1) { Alias = "contentType" }; - contentType.Variations |= ContentVariation.CultureNeutral; //supports both variant/invariant + // content type varies by Culture + // prop1 varies by Culture + // prop2 is invariant + + var contentType = new ContentType(-1) { Alias = "contentType" }; + contentType.Variations |= ContentVariation.Culture; + + var variantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop1", Variations = ContentVariation.Culture, Mandatory = true }; + var invariantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop2", Variations = ContentVariation.Nothing, Mandatory = true}; - //In real life, a property cannot be both invariant + variant and be mandatory. If this happens validation will always fail when doing TryPublishValues since the invariant value will always be empty. - //so here we are only setting properties to one or the other, not both - var variantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop1", Variations = ContentVariation.CultureNeutral, Mandatory = true }; - var invariantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop2", Variations = ContentVariation.InvariantNeutral, Mandatory = true}; - contentType.AddPropertyType(variantPropType); contentType.AddPropertyType(invariantPropType); var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; - content.SetName("hello", langFr); + content.SetCultureName("hello", langFr); - Assert.IsFalse(content.TryPublishValues(langFr)); //will fail because prop1 is mandatory + Assert.IsFalse(content.TryPublishValues(langFr)); // fails because prop1 is mandatory content.SetValue("prop1", "a", langFr); Assert.IsTrue(content.TryPublishValues(langFr)); Assert.AreEqual("a", content.GetValue("prop1", langFr, published: true)); //this will be null because we tried to publish values for a specific culture but this property is invariant Assert.IsNull(content.GetValue("prop2", published: true)); - Assert.IsFalse(content.TryPublishValues()); //will fail because prop2 is mandatory + Assert.IsFalse(content.TryPublishValues()); // fails because prop2 is mandatory content.SetValue("prop2", "b"); Assert.IsTrue(content.TryPublishValues()); Assert.AreEqual("b", content.GetValue("prop2", published: true)); @@ -346,10 +396,11 @@ namespace Umbraco.Tests.Models var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; - contentType.Variations |= ContentVariation.CultureNeutral; - propertyType.Variations |= ContentVariation.CultureNeutral; + // change - now we vary by culture + contentType.Variations |= ContentVariation.Culture; + propertyType.Variations |= ContentVariation.Culture; - content.SetValue("prop", "a"); + Assert.Throws(() => content.SetValue("prop", "a")); // invariant = no content.SetValue("prop", "a-fr", langFr); content.SetValue("prop", "a-uk", langUk); content.SetValue("prop", "a-es", langEs); @@ -359,29 +410,29 @@ namespace Umbraco.Tests.Models // works with a name // and then FR is available, and published - content.SetName("name-fr", langFr); + content.SetCultureName("name-fr", langFr); Assert.IsTrue(content.TryPublishValues(langFr)); // now UK is available too - content.SetName("name-uk", langUk); + content.SetCultureName("name-uk", langUk); // 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.GetCulturePublishDate(langFr)); + Assert.AreNotEqual(DateTime.MinValue, content.GetPublishDate(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.GetCulturePublishDate(langUk)); // not published! + Assert.IsNull(content.GetPublishDate(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.GetCulturePublishDate(langEs)); // not published! + Assert.IsNull(content.GetPublishDate(langEs)); // not published! Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited // cannot test IsCultureEdited here - as that requires the content service and repository @@ -432,7 +483,7 @@ namespace Umbraco.Tests.Models Assert.IsFalse(prop.IsValid()); // can publish, even though invalid - prop.PublishValue(); + prop.PublishValues(); } } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 983cccbdb9..805ea2d862 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -683,22 +683,24 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void GetPagedResultsByQuery_With_Variant_Names() { - //2x content types, one invariant, one variant - + // one invariant content type named "umbInvariantTextPage" + // var invariantCt = MockedContentTypes.CreateSimpleContentType("umbInvariantTextpage", "Invariant Textpage"); - invariantCt.Variations = ContentVariation.InvariantNeutral; - foreach (var p in invariantCt.PropertyTypes) p.Variations = ContentVariation.InvariantNeutral; + invariantCt.Variations = ContentVariation.Nothing; + foreach (var p in invariantCt.PropertyTypes) p.Variations = ContentVariation.Nothing; ServiceContext.FileService.SaveTemplate(invariantCt.DefaultTemplate); // else, FK violation on contentType! ServiceContext.ContentTypeService.Save(invariantCt); + // one variant (by culture) content type named "umbVariantTextPage" + // with properties, every 2nd one being variant (by culture), the other being invariant + // var variantCt = MockedContentTypes.CreateSimpleContentType("umbVariantTextpage", "Variant Textpage"); - variantCt.Variations = ContentVariation.CultureNeutral; + variantCt.Variations = ContentVariation.Culture; var propTypes = variantCt.PropertyTypes.ToList(); for (var i = 0; i < propTypes.Count; i++) { var p = propTypes[i]; - //every 2nd one is variant - p.Variations = i % 2 == 0 ? ContentVariation.CultureNeutral : ContentVariation.InvariantNeutral; + p.Variations = i % 2 == 0 ? ContentVariation.Culture : ContentVariation.Nothing; } ServiceContext.FileService.SaveTemplate(variantCt.DefaultTemplate); // else, FK violation on contentType! ServiceContext.ContentTypeService.Save(variantCt); @@ -711,6 +713,8 @@ namespace Umbraco.Tests.Persistence.Repositories var root = MockedContent.CreateSimpleContent(invariantCt); ServiceContext.ContentService.Save(root); + var children = new List(); + for (var i = 0; i < 25; i++) { var isInvariant = i % 2 == 0; @@ -732,20 +736,30 @@ namespace Umbraco.Tests.Persistence.Repositories } ServiceContext.ContentService.Save(child); + children.Add(child); } + var child1 = children[1]; + Assert.IsTrue(child1.ContentType.VariesByCulture()); + Assert.IsTrue(child1.Name.StartsWith("VAR")); + Assert.IsTrue(child1.GetCultureName("en-US").StartsWith("VAR")); + var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { var repository = CreateRepository((IScopeAccessor)provider, out _); - var query = scope.SqlContext.Query().Where(x => x.ParentId == root.Id); + var child = repository.Get(children[1].Id); // 1 is variant + Assert.IsTrue(child.ContentType.VariesByCulture()); + Assert.IsTrue(child.Name.StartsWith("VAR")); + Assert.IsTrue(child.GetCultureName("en-US").StartsWith("VAR")); try { scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + var query = scope.SqlContext.Query().Where(x => x.ParentId == root.Id); var result = repository.GetPage(query, 0, 20, out var totalRecords, "UpdateDate", Direction.Ascending, true); Assert.AreEqual(25, totalRecords); @@ -761,7 +775,7 @@ namespace Umbraco.Tests.Persistence.Repositories foreach (var p in r.Properties) { //ensure there is a value for the correct variant/invariant property - var value = p.GetValue(p.PropertyType.Variations.Has(ContentVariation.InvariantNeutral) ? null : "en-US"); + var value = p.GetValue(p.PropertyType.Variations.VariesByNothing() ? null : "en-US"); Assert.IsNotNull(value); } } diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index 2703cd448d..492f1f7dc0 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -84,8 +84,8 @@ namespace Umbraco.Tests.PublishedContent dataType }; - var propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral }; - var contentType = new ContentType(-1) { Id = 2, Alias = "alias-ct", Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral }; + var propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.Culture }; + var contentType = new ContentType(-1) { Id = 2, Alias = "alias-ct", Variations = ContentVariation.Culture }; contentType.AddPropertyType(propertyType); var contentTypes = new[] @@ -195,16 +195,16 @@ namespace Umbraco.Tests.PublishedContent // but, // if the content type / property type does not vary, then it's all invariant again // modify the content type and property type, notify the snapshot service - contentType.Variations = ContentVariation.InvariantNeutral; - propertyType.Variations = ContentVariation.InvariantNeutral; + contentType.Variations = ContentVariation.Nothing; + propertyType.Variations = ContentVariation.Nothing; snapshotService.Notify(new[] { new ContentTypeCacheRefresher.JsonPayload("IContentType", publishedContent.ContentType.Id, ContentTypeChangeTypes.RefreshMain) }); // get a new snapshot (nothing changed in the old one), get the published content again var anotherSnapshot = snapshotService.CreatePublishedSnapshot(previewToken: null); var againContent = anotherSnapshot.Content.GetById(1); - Assert.AreEqual(ContentVariation.InvariantNeutral, againContent.ContentType.Variations); - Assert.AreEqual(ContentVariation.InvariantNeutral, againContent.ContentType.GetPropertyType("prop").Variations); + Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.Variations); + Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.GetPropertyType("prop").Variations); // now, "no culture" means "invariant" Assert.AreEqual("It Works1!", againContent.Name); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index 4f895243b2..a640423515 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -98,7 +98,7 @@ namespace Umbraco.Tests.PublishedContent var doc = GetContent(true, 1); //change a doc type alias var c = (TestPublishedContent) doc.Children.ElementAt(0); - c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); + c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var dt = doc.ChildrenAsTable(Current.Services, "Child"); @@ -175,7 +175,7 @@ namespace Umbraco.Tests.PublishedContent new RawValueProperty(factory.CreatePropertyType("property3", 1), d, "value" + (indexVals + 2))); } - d.ContentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); + d.ContentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); return d; } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs index 66f7871a4b..df7e1d9d09 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs @@ -81,7 +81,7 @@ namespace Umbraco.Tests.PublishedContent pc.Setup(content => content.Path).Returns("-1,1"); pc.Setup(content => content.Parent).Returns(() => null); pc.Setup(content => content.Properties).Returns(new Collection()); - pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral)); + pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing)); return pc; } } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 4f63533693..756b775e46 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -330,11 +330,11 @@ namespace Umbraco.Tests.PublishedContent } public AutoPublishedContentType(int id, string alias, IEnumerable propertyTypes) - : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.InvariantNeutral) + : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) { } public AutoPublishedContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) - : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.InvariantNeutral) + : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) { } public override PublishedPropertyType GetPropertyType(string alias) diff --git a/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs index 032f15f5d1..4059cb1858 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs @@ -22,12 +22,12 @@ namespace Umbraco.Tests.Routing var properties = new[] { - new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.InvariantNeutral, + new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.Nothing, new PropertyValueConverterCollection(Enumerable.Empty()), Mock.Of(), Mock.Of()), }; - _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.InvariantNeutral); + _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); } protected override PublishedContentType GetPublishedContentTypeByAlias(string alias) diff --git a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs index 6b5eedc743..0c1f89f430 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs @@ -20,12 +20,12 @@ namespace Umbraco.Tests.Routing var properties = new[] { - new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.InvariantNeutral, + new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.Nothing, new PropertyValueConverterCollection(Enumerable.Empty()), Mock.Of(), Mock.Of()), }; - _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.InvariantNeutral); + _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); } protected override PublishedContentType GetPublishedContentTypeByAlias(string alias) diff --git a/src/Umbraco.Tests/Routing/UrlProviderTests.cs b/src/Umbraco.Tests/Routing/UrlProviderTests.cs index d3cd25ae92..2f1e4e3476 100644 --- a/src/Umbraco.Tests/Routing/UrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/UrlProviderTests.cs @@ -167,8 +167,7 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.CultureNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); var publishedContentCache = new Mock(); @@ -216,8 +215,7 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.CultureNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); var publishedContentCache = new Mock(); @@ -274,8 +272,7 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.CultureNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); var publishedContentCache = new Mock(); diff --git a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs index 627d95ea29..c6bd0fdb88 100644 --- a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs @@ -186,7 +186,7 @@ namespace Umbraco.Tests.Routing { new DefaultUrlProvider(settings.RequestHandler, Logger, globalSettings.Object, new SiteDomainHelper()) }, globalSettings:globalSettings.Object); - + SetDomains1(); var currentUri = new Uri(currentUrl); @@ -415,7 +415,9 @@ namespace Umbraco.Tests.Routing var result = umbracoContext.UrlProvider.GetOtherUrls(100111).ToArray(); - Assert.AreEqual(2, result.Count()); + foreach (var x in result) Console.WriteLine(x); + + Assert.AreEqual(2, result.Length); Assert.IsTrue(result.Contains("http://domain1a.com/en/1001-1-1/")); Assert.IsTrue(result.Contains("http://domain1b.com/en/1001-1-1/")); } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 50a535d029..92c38cee67 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2507,7 +2507,7 @@ namespace Umbraco.Tests.Services var languageService = ServiceContext.LocalizationService; var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; - var langFr = new Language("fr-FR"); + var langFr = new Language("fr-FR"); languageService.Save(langFr); languageService.Save(langUk); @@ -2515,15 +2515,15 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; 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 }); + contentType.Variations = ContentVariation.Culture; + contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.Culture }); contentTypeService.Save(contentType); var contentService = ServiceContext.ContentService; var content = new Content(null, -1, contentType); - content.SetName("name-us", langUk.IsoCode); - content.SetName("name-fr", langFr.IsoCode); + content.SetCultureName("name-us", langUk.IsoCode); + content.SetCultureName("name-fr", langFr.IsoCode); contentService.Save(content); //the name will be set to the default culture variant name @@ -2550,26 +2550,26 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; 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 }); + contentType.Variations = ContentVariation.Culture; contentTypeService.Save(contentType); var contentService = ServiceContext.ContentService; var content = new Content(null, -1, contentType); - content.SetName("root", langUk.IsoCode); + content.SetCultureName("root", langUk.IsoCode); contentService.Save(content); for (var i = 0; i < 5; i++) { var child = new Content(null, content, contentType); - child.SetName("child", langUk.IsoCode); + child.SetCultureName("child", langUk.IsoCode); contentService.Save(child); - Assert.AreEqual("child" + (i == 0 ? "" : " (" + (i).ToString() + ")"), child.GetName(langUk.IsoCode)); + + Assert.AreEqual("child" + (i == 0 ? "" : " (" + i + ")"), child.GetCultureName(langUk.IsoCode)); //Save it again to ensure that the unique check is not performed again against it's own name contentService.Save(child); - Assert.AreEqual("child" + (i == 0 ? "" : " (" + (i).ToString() + ")"), child.GetName(langUk.IsoCode)); + Assert.AreEqual("child" + (i == 0 ? "" : " (" + i + ")"), child.GetCultureName(langUk.IsoCode)); } } @@ -2578,7 +2578,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langFr = new Language("fr-FR"); + var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; var langUk = new Language("en-UK"); var langDe = new Language("de-DE"); @@ -2588,31 +2588,29 @@ namespace Umbraco.Tests.Services 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"); - contentType.Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral; - contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.CultureNeutral }); + contentType.Variations = ContentVariation.Culture; + contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.Culture }); + // fixme add test w/ an invariant prop contentTypeService.Save(contentType); var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage"); + var content = contentService.Create("Home US", -1, "umbTextpage"); + + // creating content with a name but no culture - will set the invariant name + // but, because that content is variant, as soon as we save, we'll need to + // replace the invariant name with whatever we have in cultures - always + // + // in fact, that would throw, because there is no name + //contentService.Save(content); // act content.SetValue("author", "Barack Obama"); content.SetValue("prop", "value-fr1", langFr.IsoCode); content.SetValue("prop", "value-uk1", langUk.IsoCode); - content.SetName("name-fr", langFr.IsoCode); - content.SetName("name-uk", langUk.IsoCode); + content.SetCultureName("name-fr", langFr.IsoCode); // and then we can save + content.SetCultureName("name-uk", langUk.IsoCode); contentService.Save(content); // content has been saved, @@ -2620,9 +2618,9 @@ namespace Umbraco.Tests.Services 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("name-fr", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetCultureName(langUk.IsoCode)); Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); @@ -2657,9 +2655,9 @@ namespace Umbraco.Tests.Services 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("name-fr", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetCultureName(langUk.IsoCode)); // we haven't published InvariantNeutral, but a document cannot be published without an invariant name, // so when we tried and published for the first time above the french culture, the french name was used @@ -2686,8 +2684,8 @@ namespace Umbraco.Tests.Services 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)); - AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw // note that content and content2 culture published dates might be slightly different due to roundtrip to database @@ -2701,14 +2699,14 @@ namespace Umbraco.Tests.Services content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.PublishName); // act - content.SetName("Home US2", null); - content.SetName("name-fr2", langFr.IsoCode); - content.SetName("name-uk2", langUk.IsoCode); + content.SetCultureName("Home US2", null); + content.SetCultureName("name-fr2", langFr.IsoCode); + content.SetCultureName("name-uk2", langUk.IsoCode); content.SetValue("author", "Barack Obama2"); content.SetValue("prop", "value-fr2", langFr.IsoCode); content.SetValue("prop", "value-uk2", langUk.IsoCode); @@ -2719,11 +2717,11 @@ namespace Umbraco.Tests.Services 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("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.PublishName); Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); @@ -2747,8 +2745,8 @@ namespace Umbraco.Tests.Services 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.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw // act @@ -2762,11 +2760,11 @@ namespace Umbraco.Tests.Services 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("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr2", content2.PublishName); Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); @@ -2790,8 +2788,8 @@ namespace Umbraco.Tests.Services 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.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2812,11 +2810,11 @@ namespace Umbraco.Tests.Services 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("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); // not null, see note above + Assert.AreEqual("name-fr2", 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 @@ -2837,8 +2835,8 @@ namespace Umbraco.Tests.Services 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.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2852,11 +2850,11 @@ namespace Umbraco.Tests.Services 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("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr2", content2.PublishName); Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); @@ -2877,8 +2875,8 @@ namespace Umbraco.Tests.Services 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.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2900,18 +2898,22 @@ namespace Umbraco.Tests.Services 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.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act - content.SetName("name-uk3", langUk.IsoCode); + content.SetCultureName("name-uk3", langUk.IsoCode); contentService.Save(content); content2 = contentService.GetById(content.Id); + // note that the 'edited' flags only change once saved - not immediately + // but they change, on what's being saved, and when getting it back + // changing the name = edited! + Assert.IsTrue(content.IsCultureEdited(langUk.IsoCode)); Assert.IsTrue(content2.IsCultureEdited(langUk.IsoCode)); } diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 6abe5add94..74401c9318 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -450,19 +450,18 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var alias = "test" + Guid.NewGuid(); var contentType = MockedContentTypes.CreateSimpleContentType(alias, alias, false); - contentType.Variations = ContentVariation.CultureNeutral; + contentType.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(contentType); var c1 = MockedContent.CreateSimpleContent(contentType, "Test", -1); - c1.SetName("Test - FR", _langFr.IsoCode); - c1.SetName("Test - ES", _langEs.IsoCode); + c1.SetCultureName("Test - FR", _langFr.IsoCode); + c1.SetCultureName("Test - ES", _langEs.IsoCode); ServiceContext.ContentService.Save(c1); var result = service.Get(c1.Id, UmbracoObjectTypes.Document); - Assert.AreEqual("Test", result.Name); + Assert.AreEqual("Test - FR", result.Name); // got name from default culture Assert.IsNotNull(result as IDocumentEntitySlim); var doc = (IDocumentEntitySlim)result; var cultureNames = doc.CultureNames; @@ -476,11 +475,11 @@ namespace Umbraco.Tests.Services var service = ServiceContext.EntityService; var contentType = MockedContentTypes.CreateSimpleContentType("test1", "Test1", false); - contentType.Variations = ContentVariation.CultureNeutral; + contentType.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(contentType); var root = MockedContent.CreateSimpleContent(contentType); - root.SetName("Root", _langFr.IsoCode); // else cannot save + root.SetCultureName("Root", _langFr.IsoCode); // else cannot save ServiceContext.ContentService.Save(root); for (int i = 0; i < 10; i++) @@ -488,12 +487,12 @@ namespace Umbraco.Tests.Services var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); if (i % 2 == 0) { - c1.SetName("Test " + i + " - FR", _langFr.IsoCode); - c1.SetName("Test " + i + " - ES", _langEs.IsoCode); + c1.SetCultureName("Test " + i + " - FR", _langFr.IsoCode); + c1.SetCultureName("Test " + i + " - ES", _langEs.IsoCode); } else { - c1.SetName("Test", _langFr.IsoCode); // else cannot save + c1.SetCultureName("Test", _langFr.IsoCode); // else cannot save } ServiceContext.ContentService.Save(c1); } diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index f21a953ae7..e79e504a69 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -83,8 +83,7 @@ namespace Umbraco.Tests.Testing.TestingTests var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, umbracoContext.VariationContextAccessor); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.InvariantNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 585e9a17d6..dedf04488c 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -59,7 +59,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime)x.Attribute("createDate") && m.UpdateDate == (DateTime)x.Attribute("updateDate") && m.Name == (string)x.Attribute("nodeName") && - m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string)x.Attribute("path") && m.Properties == new PropertyCollection() && m.ContentType == Mock.Of(mt => @@ -103,7 +103,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime) x.Attribute("createDate") && m.UpdateDate == (DateTime) x.Attribute("updateDate") && m.Name == (string) x.Attribute("nodeName") && - m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string) x.Attribute("path") && m.Properties == new PropertyCollection() && m.ContentType == Mock.Of(mt => diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index e08c6c5f05..2d440b8453 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -38,7 +38,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime)x.Attribute("createDate") && m.UpdateDate == (DateTime)x.Attribute("updateDate") && m.Name == (string)x.Attribute("nodeName") && - m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string)x.Attribute("path") && m.Properties == new PropertyCollection() && m.Published == true && diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index c6f7d8551e..0a9638fc30 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -82,7 +82,7 @@ namespace Umbraco.Tests.Web var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); Mock.Get(publishedContent).Setup(x => x.Id).Returns(1234); Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 184c442568..75b22d5404 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web.Editors public bool AllowsCultureVariation() { var contentTypes = Services.ContentTypeService.GetAll(); - return contentTypes.Any(contentType => contentType.Variations.DoesSupportCulture()); + return contentTypes.Any(contentType => contentType.VariesByCulture()); } /// @@ -310,7 +310,7 @@ namespace Umbraco.Web.Editors var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - if (contentType.Variations.Has(ContentVariation.CultureNeutral)) + if (contentType.VariesByCulture()) { //Remove all variants except for the default since currently the default must be saved before other variants can be edited //TODO: Allow for editing all variants at once ... this will be a future task @@ -709,7 +709,7 @@ namespace Umbraco.Web.Editors /// private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled) { - if (!contentItem.PersistedContent.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!contentItem.PersistedContent.ContentType.VariesByCulture()) { //its invariant, proceed normally contentItem.PersistedContent.TryPublishValues(); @@ -1037,13 +1037,13 @@ namespace Umbraco.Web.Editors private void MapPropertyValues(ContentItemSave contentItem) { //Don't update the name if it is empty - if (contentItem.Name.IsNullOrWhiteSpace() == false) + if (!contentItem.Name.IsNullOrWhiteSpace()) { - //set the name according to the culture settings - if (contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) + if (contentItem.PersistedContent.ContentType.VariesByCulture()) { - if (contentItem.Culture.IsNullOrWhiteSpace()) throw new InvalidOperationException($"Cannot save a content item that is {ContentVariation.CultureNeutral} with a culture specified"); - contentItem.PersistedContent.SetName(contentItem.Name, contentItem.Culture); + if (contentItem.Culture.IsNullOrWhiteSpace()) + throw new InvalidOperationException($"Cannot set culture name without a culture."); + contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture); } else { @@ -1074,7 +1074,7 @@ namespace Umbraco.Web.Editors } } - bool Varies(Property property) => property.PropertyType.Variations.Has(ContentVariation.CultureNeutral); + bool Varies(Property property) => property.PropertyType.VariesByCulture(); MapPropertyValues( contentItem, @@ -1276,7 +1276,7 @@ namespace Umbraco.Web.Editors { //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, // the Cuture property of ContentItemDisplay must exist (at least currently). - if (culture == null && content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (culture == null && content.ContentType.VariesByCulture()) { //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs index 2e8155e1a7..101fed8a06 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs @@ -14,12 +14,9 @@ namespace Umbraco.Web.Models.Mapping public string Resolve(IContent source, ContentItemDisplay destination, string destMember, ResolutionContext context) { var culture = context.GetCulture(); - if (culture != null && source.ContentType.Variations.Has(ContentVariation.CultureNeutral)) - { - //return the culture name being requested - return source.GetName(culture); - } - return source.Name; + return source.ContentType.VariesByCulture() && culture != null + ? source.GetCultureName(culture) + : source.Name; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs index d1673c2a5b..cb6e2938be 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Models.Mapping public IEnumerable Resolve(IContent source, ContentItemDisplay destination, IEnumerable destMember, ResolutionContext context) { - if (!source.ContentType.Variations.Has(Core.Models.ContentVariation.CultureNeutral)) + if (!source.ContentType.VariesByCulture()) return Enumerable.Empty(); var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); @@ -36,7 +36,7 @@ namespace Umbraco.Web.Models.Mapping { Language = x, Mandatory = x.Mandatory, - Name = source.GetName(x.IsoCode), + Name = source.GetCultureName(x.IsoCode), Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? PublishedState = (source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished ? PublishedState.Unpublished diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs index 5353614033..ffcd39856e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs @@ -68,11 +68,11 @@ namespace Umbraco.Web.Models.Mapping var culture = context.GetCulture(); //a culture needs to be in the context for a property type that can vary - if (culture == null && property.PropertyType.Variations.Has(ContentVariation.CultureNeutral)) + if (culture == null && property.PropertyType.VariesByCulture()) throw new InvalidOperationException($"No languageId found in mapping operation when one is required for the culture neutral property type {property.PropertyType.Alias}"); //set the culture to null if it's an invariant property type - culture = !property.PropertyType.Variations.Has(ContentVariation.CultureNeutral) ? null : culture; + culture = !property.PropertyType.VariesByCulture() ? null : culture; // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. result.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture); diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs index 2ee9e38ff5..407fd64372 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs @@ -112,7 +112,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.AllowedTemplates, opt => opt.Ignore()) .ForMember(dto => dto.DefaultTemplate, opt => opt.Ignore()) .ForMember(display => display.Notifications, opt => opt.Ignore()) - .ForMember(display => display.AllowCultureVariant, opt => opt.MapFrom(type => type.Variations.HasFlag(ContentVariation.CultureNeutral))) + .ForMember(display => display.AllowCultureVariant, opt => opt.MapFrom(type => type.VariesByCulture())) .AfterMap((source, dest) => { //sync templates diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs index fcfe9a47cc..e25568868f 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs @@ -13,12 +13,10 @@ namespace Umbraco.Web.Models.Mapping public ContentVariation Resolve(TSource source, TDestination destination, ContentVariation destMember, ResolutionContext context) { //this will always be the case, a content type will always be allowed to be invariant - var result = ContentVariation.InvariantNeutral; + var result = ContentVariation.Nothing; if (source.AllowCultureVariant) - { - result |= ContentVariation.CultureNeutral; - } + result |= ContentVariation.Culture; return result; } diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs index 63c8c5e97a..fbd7be4ecd 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs @@ -221,7 +221,7 @@ namespace Umbraco.Web.Models.Mapping SortOrder = p.SortOrder, ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, - AllowCultureVariant = p.Variations.HasFlag(Core.Models.ContentVariation.CultureNeutral) + AllowCultureVariant = p.VariesByCulture() }); } diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs index 6c0fa9915e..00472a291c 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs @@ -12,12 +12,7 @@ namespace Umbraco.Web.Models.Mapping { public ContentVariation Resolve(PropertyTypeBasic source, PropertyType destination, ContentVariation destMember, ResolutionContext context) { - //A property type should only be one type of culture variation. - //If a property type allows both variant and invariant then it generally won't be able to save because validation - //occurs when performing something like IContent.TryPublishAllValues and it will result in validation problems because - //the invariant value will not be set since in the UI only the variant values are edited if it supports it. - var result = source.AllowCultureVariant ? ContentVariation.CultureNeutral : ContentVariation.InvariantNeutral; - return result; + return source.AllowCultureVariant ? ContentVariation.Culture : ContentVariation.Nothing; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 39fb005ba3..2d24efdd67 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -191,10 +191,9 @@ namespace Umbraco.Web.PublishedCache.NuCache if (culture != null && segment != null) return; // use context values - // fixme CultureSegment? var publishedVariationContext = _content.VariationContextAccessor?.VariationContext; - if (culture == null) culture = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Culture : ""; - if (segment == null) segment = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Segment : ""; + if (culture == null) culture = _variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; + if (segment == null) segment = _variations.VariesBySegment() ? publishedVariationContext?.Segment : ""; } public override object GetValue(string culture = null, string segment = null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index 567201ef6f..f8e8cdb974 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -178,7 +178,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return _contentData.Name; var culture = VariationContextAccessor.VariationContext.Culture; @@ -194,7 +194,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return _urlSegment; var culture = VariationContextAccessor.VariationContext.Culture; @@ -258,7 +258,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return NoCultureInfos; if (_cultureInfos != null) return _cultureInfos; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index fca9458565..34efa7136a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1188,13 +1188,13 @@ namespace Umbraco.Web.PublishedCache.NuCache var names = content is IContent document ? (published - ? document.PublishCultureNames + ? document.PublishNames : document.CultureNames) : content.CultureNames; foreach (var (culture, name) in names) { - cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) }; + cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) ?? DateTime.MinValue }; } //the dictionary that will be serialized diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 1d3c747b6e..5e1708f118 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -71,7 +71,7 @@ namespace Umbraco.Web public static string GetUrlSegment(this IPublishedContent content, string culture = null) { // for invariant content, return the invariant url segment - if (!content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!content.ContentType.VariesByCulture()) return content.UrlSegment; // content.GetCulture(culture) will use the 'current' culture (via accessor) in case 'culture' diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index 7ea359fa67..9e89459774 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -87,7 +87,7 @@ namespace Umbraco.Web.Routing while (domainUris == null && n != null) // n is null at root { n = n.Parent; // move to parent node - domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, false); + domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, excludeDefault: true); } // no domains = exit diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 67bd27f959..b6d79e788a 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -135,8 +135,8 @@ namespace Umbraco.Web.Routing return null; // sanitize cultures - culture = culture.NullEmpty(); - defaultCulture = defaultCulture.NullEmpty(); + culture = culture.NullOrWhiteSpaceAsNull(); + defaultCulture = defaultCulture.NullOrWhiteSpaceAsNull(); if (uri == null) { diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 1cc487fd99..4c137b2d81 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -295,7 +295,7 @@ namespace Umbraco.Web.Routing return false; // invariant - always published - if (!domainDocument.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!domainDocument.ContentType.VariesByCulture()) return true; // variant, ensure that the culture corresponding to the domain's language is published diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 86fc51fe44..b7e26f894e 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.Routing // if content is variant, go with the current culture - and that is NOT safe, there may be // no 'main' url for that culture, deal with it later - otherwise, go with the invariant // culture, and that is safe. - var varies = content.ContentType.Variations.DoesSupportCulture(); + var varies = content.ContentType.VariesByCulture(); var culture = varies ? Thread.CurrentThread.CurrentUICulture.Name : ""; if (content.Published == false) diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 3e24e037df..4e032b8b25 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -264,7 +264,7 @@ namespace Umbraco.Web.Trees // for those items that DO support cultures, we need to get the proper name, IF it exists // otherwise, invariant is fine - if (docEntity.Variations.Has(Core.Models.ContentVariation.CultureNeutral) && + if (docEntity.Variations.VariesByCulture() && docEntity.CultureNames.TryGetValue(culture, out var name) && !string.IsNullOrWhiteSpace(name)) { diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index da7f723e12..1df1445979 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -58,10 +58,10 @@ namespace Umbraco.Web.WebApi.Binders protected override bool ValidateCultureVariant(ContentItemSave postedItem, HttpActionContext actionContext) { var contentType = postedItem.PersistedContent.GetContentType(); - if (contentType.Variations.DoesSupportCulture() && postedItem.Culture.IsNullOrWhiteSpace()) + if (contentType.VariesByCulture() && postedItem.Culture.IsNullOrWhiteSpace()) { //we cannot save a content item that is culture variant if no culture was specified in the request! - actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No 'Culture' found in request. Cannot save a content item that is of a {Core.Models.ContentVariation.CultureNeutral} content type without a specified culture."); + actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No culture found in request. Cannot save a content item that varies by culture, without a specified culture."); return false; } return true; diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index d61d21700a..a1da34d46e 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -478,14 +478,14 @@ namespace umbraco { get { - if (!_inner.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!_inner.ContentType.VariesByCulture()) return NoCultureInfos; if (_cultureInfos != null) return _cultureInfos; - return _cultureInfos = _inner.PublishCultureNames - .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value, _inner.GetCulturePublishDate(x.Key))); + return _cultureInfos = _inner.PublishNames + .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value, _inner.GetPublishDate(x.Key) ?? DateTime.MinValue)); } }