From a69019aea06ac3c16fbc70dac9a6bf9926721f06 Mon Sep 17 00:00:00 2001 From: Stephan Date: Sat, 21 Apr 2018 09:57:28 +0200 Subject: [PATCH 1/4] From int languageId to string culture --- .../Collections/CompositeStringStringKey.cs | 41 +++ src/Umbraco.Core/IO/MediaFileSystem.cs | 20 +- .../Media/UploadAutoFillProperties.cs | 62 ++-- src/Umbraco.Core/Models/Content.cs | 141 +++++---- src/Umbraco.Core/Models/ContentBase.cs | 64 ++-- src/Umbraco.Core/Models/ContentExtensions.cs | 8 +- src/Umbraco.Core/Models/ContentTypeBase.cs | 4 +- src/Umbraco.Core/Models/IContent.cs | 24 +- src/Umbraco.Core/Models/IContentBase.cs | 20 +- src/Umbraco.Core/Models/IContentTypeBase.cs | 2 +- src/Umbraco.Core/Models/Property.cs | 91 +++--- src/Umbraco.Core/Models/PropertyType.cs | 4 +- .../PublishedContent/IPublishedProperty.cs | 8 +- .../PublishedContent/PublishedPropertyBase.cs | 8 +- .../PublishedContent/RawValueProperty.cs | 16 +- .../Persistence/Factories/PropertyFactory.cs | 22 +- .../Repositories/ILanguageRepository.cs | 4 +- .../Implement/ContentRepositoryBase.cs | 12 +- .../Implement/DocumentRepository.cs | 20 +- .../Implement/LanguageRepository.cs | 20 +- .../Repositories/Implement/MediaRepository.cs | 8 +- .../Implement/MemberRepository.cs | 8 +- .../PropertyEditors/DataValueEditor.cs | 14 +- .../PropertyEditors/IDataValueEditor.cs | 2 +- src/Umbraco.Core/Services/IContentService.cs | 2 +- .../Services/ILocalizationService.cs | 2 +- .../Services/Implement/ContentService.cs | 8 +- .../Services/Implement/LocalizationService.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Composing/TypeLoaderTests.cs | 2 +- src/Umbraco.Tests/Models/VariationTests.cs | 92 +++--- .../Repositories/MediaRepositoryTest.cs | 2 +- .../Repositories/MemberRepositoryTest.cs | 2 +- .../Repositories/TagRepositoryTest.cs | 3 +- .../Repositories/UserRepositoryTest.cs | 2 +- .../Published/NestedContentTests.cs | 8 +- .../PublishedContentTestElements.cs | 8 +- .../PublishedContent/PublishedContentTests.cs | 2 +- .../Services/ContentServiceTests.cs | 276 +++++++++--------- src/Umbraco.Tests/TestHelpers/TestHelper.cs | 4 +- src/Umbraco.Web/Editors/ContentController.cs | 33 ++- src/Umbraco.Web/Models/ContentExtensions.cs | 1 - .../Mapping/ContentPropertyBasicConverter.cs | 6 +- .../Models/Mapping/ContextMapper.cs | 10 +- .../Models/Mapping/VariationResolver.cs | 9 +- .../PropertyEditors/DateValueEditor.cs | 4 +- .../FileUploadPropertyEditor.cs | 10 +- .../ImageCropperPropertyEditor.cs | 8 +- .../ImageCropperPropertyValueEditor.cs | 4 +- .../MultipleTextStringPropertyEditor.cs | 4 +- .../NestedContentPropertyEditor.cs | 4 +- .../PublishValuesMultipleValueEditor.cs | 6 +- .../PropertyEditors/RichTextPropertyEditor.cs | 6 +- .../PropertyEditors/TextOnlyValueEditor.cs | 6 +- .../NuCache/DataSource/BTree.cs | 4 +- .../NuCache/DataSource/PropertyData.cs | 6 +- .../PublishedCache/NuCache/Property.cs | 57 ++-- .../NuCache/PublishedSnapshotService.cs | 2 +- .../PublishedElementPropertyBase.cs | 8 +- .../XmlPublishedCache/XmlPublishedProperty.cs | 8 +- .../PublishedContentPropertyExtension.cs | 14 +- src/Umbraco.Web/PublishedElementExtensions.cs | 75 +++-- .../WebApi/Binders/ContentItemBinder.cs | 2 +- src/Umbraco.Web/umbraco.presentation/page.cs | 8 +- 64 files changed, 708 insertions(+), 626 deletions(-) create mode 100644 src/Umbraco.Core/Collections/CompositeStringStringKey.cs diff --git a/src/Umbraco.Core/Collections/CompositeStringStringKey.cs b/src/Umbraco.Core/Collections/CompositeStringStringKey.cs new file mode 100644 index 0000000000..11f123f2d0 --- /dev/null +++ b/src/Umbraco.Core/Collections/CompositeStringStringKey.cs @@ -0,0 +1,41 @@ +using System; + +namespace Umbraco.Core.Collections +{ + /// + /// Represents a composite key of (string, string) for fast dictionaries. + /// + /// + /// The string parts of the key are case-insensitive. + /// Null is a valid value for both parts. + /// + public struct CompositeStringStringKey : IEquatable + { + private readonly string _key1; + private readonly string _key2; + + /// + /// Initializes a new instance of the struct. + /// + public CompositeStringStringKey(string key1, string key2) + { + _key1 = key1?.ToLowerInvariant() ?? "NULL"; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; + } + + public bool Equals(CompositeStringStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object obj) + => obj is CompositeStringStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => _key2.GetHashCode() * 31 + _key1.GetHashCode(); + + public static bool operator ==(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + } +} diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index b96b0d54bc..5534936a04 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -335,35 +335,35 @@ namespace Umbraco.Core.IO // fixme - what's below belongs to the upload property editor, not the media filesystem! - public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream, int? languageId = null, string segment = null) + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) { var property = GetProperty(content, propertyTypeAlias); - var oldpath = property.GetValue(languageId, segment) is string svalue ? GetRelativePath(svalue) : null; + var oldpath = property.GetValue(culture, segment) is string svalue ? GetRelativePath(svalue) : null; var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); - property.SetValue(GetUrl(filepath), languageId, segment); - SetUploadFile(content, property, filepath, filestream, languageId, segment); + property.SetValue(GetUrl(filepath), culture, segment); + SetUploadFile(content, property, filepath, filestream, culture, segment); } - public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath, int? languageId = null, string segment = null) + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath, string culture = null, string segment = null) { var property = GetProperty(content, propertyTypeAlias); // fixme delete? - var oldpath = property.GetValue(languageId, segment) is string svalue ? GetRelativePath(svalue) : null; + var oldpath = property.GetValue(culture, segment) is string svalue ? GetRelativePath(svalue) : null; if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) DeleteFile(oldpath); - property.SetValue(GetUrl(filepath), languageId, segment); + property.SetValue(GetUrl(filepath), culture, segment); using (var filestream = OpenFile(filepath)) { - SetUploadFile(content, property, filepath, filestream, languageId, segment); + SetUploadFile(content, property, filepath, filestream, culture, segment); } } // sets a file for the FileUpload property editor // ie generates thumbnails and populates autofill properties - private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream, int? languageId = null, string segment = null) + private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream, string culture = null, string segment = null) { // will use filepath for extension, and filestream for length - UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, languageId, segment); + UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, culture, segment); } #endregion diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 3fdaf7b3d3..4ab2bf7a7c 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -41,9 +41,9 @@ namespace Umbraco.Core.Media /// /// The content item. /// The property type alias. - /// Variation language. + /// Variation language. /// Variation segment. - public void Reset(IContentBase content, string propertyTypeAlias, int? languageId, string segment) + public void Reset(IContentBase content, string propertyTypeAlias, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (propertyTypeAlias == null) throw new ArgumentNullException(nameof(propertyTypeAlias)); @@ -53,7 +53,7 @@ namespace Umbraco.Core.Media if (autoFillConfig == null) return; // nothing // reset - Reset(content, autoFillConfig, languageId, segment); + Reset(content, autoFillConfig, culture, segment); } /// @@ -61,14 +61,14 @@ namespace Umbraco.Core.Media /// /// The content item. /// The auto-fill configuration. - /// Variation language. + /// Variation language. /// Variation segment. - public void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig, int? languageId, string segment) + public void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); - ResetProperties(content, autoFillConfig, languageId, segment); + ResetProperties(content, autoFillConfig, culture, segment); } /// @@ -77,9 +77,9 @@ namespace Umbraco.Core.Media /// The content item. /// The property type alias. /// The filesystem-relative filepath, or null to clear properties. - /// Variation language. + /// Variation language. /// Variation segment. - public void Populate(IContentBase content, string propertyTypeAlias, string filepath, int? languageId, string segment) + public void Populate(IContentBase content, string propertyTypeAlias, string filepath, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (propertyTypeAlias == null) throw new ArgumentNullException(nameof(propertyTypeAlias)); @@ -92,7 +92,7 @@ namespace Umbraco.Core.Media if (autoFillConfig == null) return; // nothing // populate - Populate(content, autoFillConfig, filepath, languageId, segment); + Populate(content, autoFillConfig, filepath, culture, segment); } /// @@ -102,9 +102,9 @@ namespace Umbraco.Core.Media /// The property type alias. /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. - /// Variation language. + /// Variation language. /// Variation segment. - public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, int? languageId, string segment) + public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (propertyTypeAlias == null) throw new ArgumentNullException(nameof(propertyTypeAlias)); @@ -117,7 +117,7 @@ namespace Umbraco.Core.Media if (autoFillConfig == null) return; // nothing // populate - Populate(content, autoFillConfig, filepath, filestream, languageId, segment); + Populate(content, autoFillConfig, filepath, filestream, culture, segment); } /// @@ -127,9 +127,9 @@ namespace Umbraco.Core.Media /// The auto-fill configuration. /// The filesystem path to the uploaded file. /// The parameter is the path relative to the filesystem. - /// Variation language. + /// Variation language. /// Variation segment. - public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, int? languageId, string segment) + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); @@ -137,7 +137,7 @@ namespace Umbraco.Core.Media // no file = reset, file = auto-fill if (filepath.IsNullOrWhiteSpace()) { - ResetProperties(content, autoFillConfig, languageId, segment); + ResetProperties(content, autoFillConfig, culture, segment); } else { @@ -148,13 +148,13 @@ namespace Umbraco.Core.Media { var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); var size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; - SetProperties(content, autoFillConfig, size, filestream.Length, extension, languageId, segment); + SetProperties(content, autoFillConfig, size, filestream.Length, extension, culture, segment); } } catch (Exception ex) { _logger.Error(typeof(UploadAutoFillProperties), $"Could not populate upload auto-fill properties for file \"{filepath}\".", ex); - ResetProperties(content, autoFillConfig, languageId, segment); + ResetProperties(content, autoFillConfig, culture, segment); } } } @@ -166,9 +166,9 @@ namespace Umbraco.Core.Media /// /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. - /// Variation language. + /// Variation language. /// Variation segment. - public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, int? languageId, string segment) + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); @@ -176,50 +176,50 @@ namespace Umbraco.Core.Media // no file = reset, file = auto-fill if (filepath.IsNullOrWhiteSpace() || filestream == null) { - ResetProperties(content, autoFillConfig, languageId, segment); + ResetProperties(content, autoFillConfig, culture, segment); } else { var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); var size = _mediaFileSystem.IsImageFile(extension) ? (Size?)_mediaFileSystem.GetDimensions(filestream) : null; - SetProperties(content, autoFillConfig, size, filestream.Length, extension, languageId, segment); + SetProperties(content, autoFillConfig, size, filestream.Length, extension, culture, segment); } } - private static void SetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig, Size? size, long length, string extension, int? languageId, string segment) + private static void SetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig, Size? size, long length, string extension, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) - content.Properties[autoFillConfig.WidthFieldAlias].SetValue(size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty, languageId, segment); + content.Properties[autoFillConfig.WidthFieldAlias].SetValue(size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty, culture, segment); if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) - content.Properties[autoFillConfig.HeightFieldAlias].SetValue(size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty, languageId, segment); + content.Properties[autoFillConfig.HeightFieldAlias].SetValue(size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty, culture, segment); if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) - content.Properties[autoFillConfig.LengthFieldAlias].SetValue(length, languageId, segment); + content.Properties[autoFillConfig.LengthFieldAlias].SetValue(length, culture, segment); if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) - content.Properties[autoFillConfig.ExtensionFieldAlias].SetValue(extension, languageId, segment); + content.Properties[autoFillConfig.ExtensionFieldAlias].SetValue(extension, culture, segment); } - private static void ResetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig, int? languageId, string segment) + private static void ResetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string culture, string segment) { if (content == null) throw new ArgumentNullException(nameof(content)); if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) - content.Properties[autoFillConfig.WidthFieldAlias].SetValue(string.Empty, languageId, segment); + content.Properties[autoFillConfig.WidthFieldAlias].SetValue(string.Empty, culture, segment); if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) - content.Properties[autoFillConfig.HeightFieldAlias].SetValue(string.Empty, languageId, segment); + content.Properties[autoFillConfig.HeightFieldAlias].SetValue(string.Empty, culture, segment); if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) - content.Properties[autoFillConfig.LengthFieldAlias].SetValue(string.Empty, languageId, segment); + content.Properties[autoFillConfig.LengthFieldAlias].SetValue(string.Empty, culture, segment); if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) - content.Properties[autoFillConfig.ExtensionFieldAlias].SetValue(string.Empty, languageId, segment); + content.Properties[autoFillConfig.ExtensionFieldAlias].SetValue(string.Empty, culture, segment); } } } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 142d7540c2..1f106124b4 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Models private PublishedState _publishedState; private DateTime? _releaseDate; private DateTime? _expireDate; - private Dictionary _publishNames; + private Dictionary _publishNames; private static readonly Lazy Ps = new Lazy(); @@ -201,24 +201,24 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; /// - public string GetPublishName(int? languageId) + public string GetPublishName(string culture) { - if (languageId == null) return PublishName; + if (culture == null) return PublishName; if (_publishNames == null) return null; - return _publishNames.TryGetValue(languageId.Value, out var name) ? name : null; + return _publishNames.TryGetValue(culture, out var name) ? name : null; } // sets a publish name // internal for repositories - internal void SetPublishName(int? languageId, string name) + internal void SetPublishName(string culture, string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - if (languageId == null) + if (culture == null) { PublishName = name; return; @@ -227,22 +227,22 @@ namespace Umbraco.Core.Models // private method, assume that culture is valid if (_publishNames == null) - _publishNames = new Dictionary(); + _publishNames = new Dictionary(StringComparer.OrdinalIgnoreCase); - _publishNames[languageId.Value] = name; + _publishNames[culture] = name; } // clears a publish name - private void ClearPublishName(int? languageId) + private void ClearPublishName(string culture) { - if (languageId == null) + if (culture == null) { PublishName = null; return; } if (_publishNames == null) return; - _publishNames.Remove(languageId.Value); + _publishNames.Remove(culture); if (_publishNames.Count == 0) _publishNames = null; } @@ -255,12 +255,12 @@ namespace Umbraco.Core.Models } /// - public bool IsCultureAvailable(int? languageId) - => !string.IsNullOrWhiteSpace(GetName(languageId)); + public bool IsCultureAvailable(string culture) + => !string.IsNullOrWhiteSpace(GetName(culture)); /// - public bool IsCulturePublished(int? languageId) - => !string.IsNullOrWhiteSpace(GetPublishName(languageId)); + public bool IsCulturePublished(string culture) + => !string.IsNullOrWhiteSpace(GetPublishName(culture)); [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -275,57 +275,72 @@ namespace Umbraco.Core.Models if (ValidateAll().Any()) return false; - // property.PublishAllValues only deals with supported variations (if any) - foreach (var property in Properties) - property.PublishAllValues(); - // Name and PublishName are managed by the repository, but Names and PublishNames // must be managed here as they depend on the existing / supported variations. - PublishName = Name; - foreach (var (languageId, name) in Names) - SetPublishName(languageId, name); + if (string.IsNullOrWhiteSpace(Name)) + throw new InvalidOperationException($"Cannot publish invariant culture without a name."); + PublishName = Name; + foreach (var (culture, name) in Names) + { + if (string.IsNullOrWhiteSpace(name)) + throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); + SetPublishName(culture, name); + } + // property.PublishAllValues only deals with supported variations (if any) + foreach (var property in Properties) + property.PublishAllValues(); + _publishedState = PublishedState.Publishing; return true; } /// - public virtual bool PublishValues(int? languageId = null, string segment = null) + public virtual bool PublishValues(string culture = null, string segment = null) { // the variation should be supported by the content type - ContentType.ValidateVariation(languageId, segment, throwIfInvalid: true); + ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); // the values we want to publish should be valid - if (Validate(languageId, segment).Any()) + if (Validate(culture, segment).Any()) return false; - // property.PublishValue throws on invalid variation, so filter them out - foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(languageId, segment, throwIfInvalid: false))) - property.PublishValue(languageId, 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. - SetPublishName(languageId, GetName(languageId)); + if (segment == null) + { + var name = GetName(culture); + if (string.IsNullOrWhiteSpace(name)) + throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); + SetPublishName(culture, name); + } + // 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); + _publishedState = PublishedState.Publishing; return true; } /// - public virtual bool PublishCultureValues(int? languageId = null) + public virtual bool PublishCultureValues(string culture = null) { // the values we want to publish should be valid - if (ValidateCulture(languageId).Any()) + if (ValidateCulture(culture).Any()) return false; - // property.PublishCultureValues only deals with supported variations (if any) - foreach (var property in Properties) - property.PublishCultureValues(languageId); - // Name and PublishName are managed by the repository, but Names and PublishNames // must be managed here as they depend on the existing / supported variations. - SetPublishName(languageId, GetName(languageId)); + var name = GetName(culture); + if (string.IsNullOrWhiteSpace(name)) + throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); + SetPublishName(culture, name); + // property.PublishCultureValues only deals with supported variations (if any) + foreach (var property in Properties) + property.PublishCultureValues(culture); + _publishedState = PublishedState.Publishing; return true; } @@ -345,32 +360,32 @@ namespace Umbraco.Core.Models } /// - public virtual void ClearPublishedValues(int? languageId = null, string segment = null) + public virtual void ClearPublishedValues(string culture = null, string segment = null) { // the variation should be supported by the content type - ContentType.ValidateVariation(languageId, segment, throwIfInvalid: true); + 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(languageId, segment, throwIfInvalid: false))) - property.ClearPublishedValue(languageId, segment); + 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(languageId); + ClearPublishName(culture); _publishedState = PublishedState.Publishing; } /// - public virtual void ClearCulturePublishedValues(int? languageId = null) + public virtual void ClearCulturePublishedValues(string culture = null) { // property.ClearPublishedCultureValues only deals with supported variations (if any) foreach (var property in Properties) - property.ClearPublishedCultureValues(languageId); + 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(languageId); + ClearPublishName(culture); _publishedState = PublishedState.Publishing; } @@ -397,8 +412,8 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) foreach (var pvalue in property.Values) - if (property.PropertyType.ValidateVariation(pvalue.LanguageId, pvalue.Segment, false)) - property.SetValue(null, pvalue.LanguageId, pvalue.Segment); + if (property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + property.SetValue(null, pvalue.Culture, pvalue.Segment); // copy other properties var otherProperties = other.Properties; @@ -407,10 +422,10 @@ namespace Umbraco.Core.Models var alias = otherProperty.PropertyType.Alias; foreach (var pvalue in otherProperty.Values) { - if (!otherProperty.PropertyType.ValidateVariation(pvalue.LanguageId, pvalue.Segment, false)) + if (!otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) continue; var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - SetValue(alias, value, pvalue.LanguageId, pvalue.Segment); + SetValue(alias, value, pvalue.Culture, pvalue.Segment); } } @@ -422,7 +437,7 @@ namespace Umbraco.Core.Models } /// - public virtual void CopyValues(IContent other, int? languageId = null, string segment = null) + public virtual void CopyValues(IContent other, string culture = null, string segment = null) { if (other.ContentTypeId != ContentTypeId) throw new InvalidOperationException("Cannot copy values from a different content type."); @@ -437,31 +452,31 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) { - if (!property.PropertyType.ValidateVariation(languageId, segment, false)) + if (!property.PropertyType.ValidateVariation(culture, segment, false)) continue; foreach (var pvalue in property.Values) - if (pvalue.LanguageId == languageId && pvalue.Segment == segment) - property.SetValue(null, pvalue.LanguageId, pvalue.Segment); + if (pvalue.Culture.InvariantEquals(culture) && pvalue.Segment.InvariantEquals(segment)) + property.SetValue(null, pvalue.Culture, pvalue.Segment); } // copy other properties var otherProperties = other.Properties; foreach (var otherProperty in otherProperties) { - if (!otherProperty.PropertyType.ValidateVariation(languageId, segment, false)) + if (!otherProperty.PropertyType.ValidateVariation(culture, segment, false)) continue; var alias = otherProperty.PropertyType.Alias; - SetValue(alias, otherProperty.GetValue(languageId, segment, published), languageId, segment); + SetValue(alias, otherProperty.GetValue(culture, segment, published), culture, segment); } // copy name - SetName(languageId, other.GetName(languageId)); + SetName(culture, other.GetName(culture)); } /// - public virtual void CopyCultureValues(IContent other, int? languageId = null) + public virtual void CopyCultureValues(IContent other, string culture = null) { if (other.ContentTypeId != ContentTypeId) throw new InvalidOperationException("Cannot copy values from a different content type."); @@ -473,8 +488,8 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) foreach (var pvalue in property.Values) - if (pvalue.LanguageId == languageId && property.PropertyType.ValidateVariation(pvalue.LanguageId, pvalue.Segment, false)) - property.SetValue(null, pvalue.LanguageId, pvalue.Segment); + if (pvalue.Culture.InvariantEquals(culture) && property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + property.SetValue(null, pvalue.Culture, pvalue.Segment); // copy other properties var otherProperties = other.Properties; @@ -483,15 +498,15 @@ namespace Umbraco.Core.Models var alias = otherProperty.PropertyType.Alias; foreach (var pvalue in otherProperty.Values) { - if (pvalue.LanguageId != languageId || !otherProperty.PropertyType.ValidateVariation(pvalue.LanguageId, pvalue.Segment, false)) + if (pvalue.Culture != culture || !otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) continue; var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - SetValue(alias, value, pvalue.LanguageId, pvalue.Segment); + SetValue(alias, value, pvalue.Culture, pvalue.Segment); } } // copy name - SetName(languageId, other.GetName(languageId)); + SetName(culture, other.GetName(culture)); } /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 8203cba985..32a26f9dc0 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -18,14 +18,14 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")] public abstract class ContentBase : TreeEntityBase, IContentBase { - protected static readonly Dictionary NoNames = new Dictionary(); + protected static readonly Dictionary NoNames = new Dictionary(); private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; private PropertyCollection _properties; - private Dictionary _names; + private Dictionary _names; /// /// Initializes a new instance of the class. @@ -67,7 +67,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); - public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.Names); + public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.Names); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -123,26 +123,26 @@ namespace Umbraco.Core.Models /// [DataMember] - public virtual IReadOnlyDictionary Names + public virtual IReadOnlyDictionary Names { get => _names ?? NoNames; set { - foreach (var (languageId, name) in value) - SetName(languageId, name); + foreach (var (culture, name) in value) + SetName(culture, name); } } /// - public virtual void SetName(int? languageId, string name) + public virtual void SetName(string culture, string name) { if (string.IsNullOrWhiteSpace(name)) { - ClearName(languageId); + ClearName(culture); return; } - if (languageId == null) + if (culture == null) { Name = name; return; @@ -152,22 +152,22 @@ namespace Umbraco.Core.Models throw new NotSupportedException("Content type does not support varying name by culture."); if (_names == null) - _names = new Dictionary(); + _names = new Dictionary(StringComparer.OrdinalIgnoreCase); - _names[languageId.Value] = name; + _names[culture] = name; OnPropertyChanged(Ps.Value.NamesSelector); } - private void ClearName(int? languageId) + private void ClearName(string culture) { - if (languageId == null) + if (culture == null) { Name = null; return; } if (_names == null) return; - _names.Remove(languageId.Value); + _names.Remove(culture); if (_names.Count == 0) _names = null; } @@ -179,11 +179,11 @@ namespace Umbraco.Core.Models } /// - public virtual string GetName(int? languageId) + public virtual string GetName(string culture) { - if (languageId == null) return Name; + if (culture == null) return Name; if (_names == null) return null; - return _names.TryGetValue(languageId.Value, out var name) ? name : null; + return _names.TryGetValue(culture, out var name) ? name : null; } /// @@ -207,29 +207,29 @@ namespace Umbraco.Core.Models => Properties.Contains(propertyTypeAlias); /// - public virtual object GetValue(string propertyTypeAlias, int? languageId = null, string segment = null, bool published = false) + public virtual object GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) { return Properties.TryGetValue(propertyTypeAlias, out var property) - ? property.GetValue(languageId, segment, published) + ? property.GetValue(culture, segment, published) : null; } /// - public virtual TValue GetValue(string propertyTypeAlias, int? languageId = null, string segment = null, bool published = false) + public virtual TValue GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) { if (!Properties.TryGetValue(propertyTypeAlias, out var property)) return default; - var convertAttempt = property.GetValue(languageId, segment, published).TryConvertTo(); + var convertAttempt = property.GetValue(culture, segment, published).TryConvertTo(); return convertAttempt.Success ? convertAttempt.Result : default; } /// - public virtual void SetValue(string propertyTypeAlias, object value, int? languageId = null, string segment = null) + public virtual void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null) { if (Properties.Contains(propertyTypeAlias)) { - Properties[propertyTypeAlias].SetValue(value, languageId, segment); + Properties[propertyTypeAlias].SetValue(value, culture, segment); return; } @@ -238,7 +238,7 @@ namespace Umbraco.Core.Models throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); var property = propertyType.CreateProperty(); - property.SetValue(value, languageId, segment); + property.SetValue(value, culture, segment); Properties.Add(property); } @@ -249,17 +249,17 @@ namespace Umbraco.Core.Models /// /// Sets the posted file value of a property. /// - public virtual void SetValue(string propertyTypeAlias, HttpPostedFile value, int? languageId = null, string segment = null) + public virtual void SetValue(string propertyTypeAlias, HttpPostedFile value, string culture = null, string segment = null) { - ContentExtensions.SetValue(this, propertyTypeAlias, new HttpPostedFileWrapper(value), languageId, segment); + ContentExtensions.SetValue(this, propertyTypeAlias, new HttpPostedFileWrapper(value), culture, segment); } /// /// Sets the posted file value of a property. /// - public virtual void SetValue(string propertyTypeAlias, HttpPostedFileBase value, int? languageId = null, string segment = null) + public virtual void SetValue(string propertyTypeAlias, HttpPostedFileBase value, string culture = null, string segment = null) { - ContentExtensions.SetValue(this, propertyTypeAlias, value, languageId, segment); + ContentExtensions.SetValue(this, propertyTypeAlias, value, culture, segment); } #endregion @@ -271,14 +271,14 @@ namespace Umbraco.Core.Models return Properties.Where(x => !x.IsAllValid()).ToArray(); } - public virtual Property[] Validate(int? languageId = null, string segment = null) + public virtual Property[] Validate(string culture = null, string segment = null) { - return Properties.Where(x => !x.IsValid(languageId, segment)).ToArray(); + return Properties.Where(x => !x.IsValid(culture, segment)).ToArray(); } - public virtual Property[] ValidateCulture(int? languageId = null) + public virtual Property[] ValidateCulture(string culture = null) { - return Properties.Where(x => !x.IsCultureValid(languageId)).ToArray(); + return Properties.Where(x => !x.IsCultureValid(culture)).ToArray(); } #endregion diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 5b7e825d68..0940675346 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -284,7 +284,7 @@ namespace Umbraco.Core.Models /// /// Sets the posted file value of a property. /// - public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value, int? languageId = null, string segment = null) + public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value, string culture = null, string segment = null) { // ensure we get the filename without the path in IE in intranet mode // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie @@ -303,7 +303,7 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? - MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream, languageId, segment); + MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream, culture, segment); } /// @@ -312,7 +312,7 @@ namespace Umbraco.Core.Models /// This really is for FileUpload fields only, and should be obsoleted. For anything else, /// you need to store the file by yourself using Store and then figure out /// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself. - public static void SetValue(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, int? languageId = null, string segment = null) + public static void SetValue(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) { if (filename == null || filestream == null) return; @@ -321,7 +321,7 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? - MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, filestream, languageId, segment); + MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, filestream, culture, segment); } /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index b686b97108..7d0ffea31e 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -204,10 +204,10 @@ namespace Umbraco.Core.Models /// /// Validates that a variation is valid for the content type. /// - public bool ValidateVariation(int? languageId, string segment, bool throwIfInvalid) + public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) { ContentVariation variation; - if (languageId.HasValue) + if (culture != null) { variation = segment != null ? ContentVariation.CultureSegment diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 3f7a335620..3df81f7314 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -59,7 +59,7 @@ namespace Umbraco.Core.Models /// Gets the date and time the content was published. /// DateTime? PublishDate { get; } - + /// /// Gets or sets the date and time the content item should be published. /// @@ -87,7 +87,7 @@ namespace Umbraco.Core.Models /// A culture becomes available whenever the content name for this culture is /// non-null, and it becomes unavailable whenever the content name is null. /// - bool IsCultureAvailable(int? languageId); + bool IsCultureAvailable(string culture); /// /// Gets a value indicating whether a given culture is published. @@ -97,17 +97,17 @@ namespace Umbraco.Core.Models /// and the content published name for this culture is non-null. It becomes non-published /// whenever values for this culture are unpublished. /// - bool IsCulturePublished(int? languageId); + bool IsCulturePublished(string culture); /// /// Gets the name of the published version of the content for a given culture. /// /// /// When editing the content, the name can change, but this will not until the content is published. - /// When is null, gets the invariant + /// When is null, gets the invariant /// language, which is the value of the property. /// - string GetPublishName(int? languageId); + string GetPublishName(string culture); /// /// Gets the published names of the content. @@ -116,7 +116,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 PublishNames { get; } + IReadOnlyDictionary PublishNames { get; } // fixme - these two should move to some kind of service @@ -159,7 +159,7 @@ namespace Umbraco.Core.Models /// The document must then be published via the content service. /// Values are not published if they are not valid. /// - bool PublishValues(int? languageId = null, string segment = null); + bool PublishValues(string culture = null, string segment = null); /// /// Publishes the culture/any values. @@ -169,7 +169,7 @@ namespace Umbraco.Core.Models /// The document must then be published via the content service. /// Values are not published if they are not valie. /// - bool PublishCultureValues(int? languageId = null); + bool PublishCultureValues(string culture = null); /// /// Clears all published values. @@ -179,12 +179,12 @@ namespace Umbraco.Core.Models /// /// Clears published values. /// - void ClearPublishedValues(int? languageId = null, string segment = null); + void ClearPublishedValues(string culture = null, string segment = null); /// /// Clears the culture/any published values. /// - void ClearCulturePublishedValues(int? languageId = null); + void ClearCulturePublishedValues(string culture = null); /// /// Copies values from another document. @@ -194,11 +194,11 @@ namespace Umbraco.Core.Models /// /// Copies values from another document. /// - void CopyValues(IContent other, int? languageId = null, string segment = null); + void CopyValues(IContent other, string culture = null, string segment = null); /// /// Copies culture/any values from another document. /// - void CopyCultureValues(IContent other, int? languageId = null); + void CopyCultureValues(IContent other, string culture = null); } } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index aee8acd35e..f8bb0e6985 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -31,19 +31,19 @@ namespace Umbraco.Core.Models /// Sets the name of the content item for a specified language. /// /// - /// When is null, sets the invariant + /// When is null, sets the invariant /// language, which sets the property. /// - void SetName(int? languageId, string value); + void SetName(string culture, string value); /// /// Gets the name of the content item for a specified language. /// /// - /// When is null, gets the invariant + /// When is null, gets the invariant /// language, which is the value of the property. /// - string GetName(int? languageId); + string GetName(string culture); /// /// Gets or sets the names of the content item. @@ -52,7 +52,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot get nor set the invariant /// name, which must be get or set via the property. /// - IReadOnlyDictionary Names { get; set; } + IReadOnlyDictionary Names { get; set; } /// /// List of properties, which make up all the data available for this Content object @@ -82,17 +82,17 @@ namespace Umbraco.Core.Models /// /// Gets the value of a Property /// - object GetValue(string propertyTypeAlias, int? languageId = null, string segment = null, bool published = false); + object GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false); /// /// Gets the typed value of a Property /// - TValue GetValue(string propertyTypeAlias, int? languageId = null, string segment = null, bool published = false); + TValue GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false); /// /// Sets the (edited) value of a Property /// - void SetValue(string propertyTypeAlias, object value, int? languageId = null, string segment = null); + void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null); /// /// Gets a value indicating whether the content and all its properties values are valid. @@ -102,11 +102,11 @@ namespace Umbraco.Core.Models /// /// Gets a value indicating whether the content and its properties values are valid. /// - Property[] Validate(int? languageId = null, string segment = null); + Property[] Validate(string culture = null, string segment = null); /// /// Gets a value indicating whether the content and its culture/any properties values are valid. /// - Property[] ValidateCulture(int? languageId = null); + Property[] ValidateCulture(string culture = null); } } diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index 68e5923d66..7c8cc8eafe 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -51,7 +51,7 @@ namespace Umbraco.Core.Models /// /// Validates that a variation is valid for the content type. /// - bool ValidateVariation(int? languageId, string segment, bool throwIfInvalid); + bool ValidateVariation(string culture, string segment, bool throwIfInvalid); /// /// 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 9e3f5fc2e0..709c37daaa 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Models { private List _values = new List(); private PropertyValue _pvalue; - private Dictionary _vvalues; + private Dictionary _vvalues; private static readonly Lazy Ps = new Lazy(); @@ -38,9 +38,14 @@ namespace Umbraco.Core.Models public class PropertyValue { + private string _culture; private string _segment; - public int? LanguageId { get; internal set; } + public string Culture + { + get => _culture; + internal set => _culture = value?.ToLowerInvariant(); + } public string Segment { get => _segment; @@ -50,7 +55,7 @@ namespace Umbraco.Core.Models public object PublishedValue { get; internal set; } public PropertyValue Clone() - => new PropertyValue { LanguageId = LanguageId, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; + => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; } // ReSharper disable once ClassNeverInstantiated.Local @@ -96,10 +101,10 @@ 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.LanguageId, x.Segment, false)).ToList(); - _pvalue = _values.FirstOrDefault(x => !x.LanguageId.HasValue && x.Segment == null); + _values = value.Where(x => PropertyType.ValidateVariation(x.Culture, x.Segment, false)).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 CompositeIntStringKey(x.LanguageId, x.Segment), x => x) + ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeStringStringKey(x.Culture, x.Segment), x => x) : null; } } @@ -128,12 +133,12 @@ namespace Umbraco.Core.Models /// /// Gets the value. /// - public object GetValue(int? languageId = null, string segment = null, bool published = false) + public object GetValue(string culture = null, string segment = null, bool published = false) { - if (!PropertyType.ValidateVariation(languageId, segment, false)) return null; - if (!languageId.HasValue && segment == null) return GetPropertyValue(_pvalue, published); + if (!PropertyType.ValidateVariation(culture, segment, false)) return null; + if (culture == null && segment == null) return GetPropertyValue(_pvalue, published); if (_vvalues == null) return null; - return _vvalues.TryGetValue(new CompositeIntStringKey(languageId, segment), out var pvalue) + return _vvalues.TryGetValue(new CompositeStringStringKey(culture, segment), out var pvalue) ? GetPropertyValue(pvalue, published) : null; } @@ -159,7 +164,7 @@ namespace Umbraco.Core.Models if (_vvalues != null) { var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) PublishPropertyValue(pvalue); @@ -168,29 +173,29 @@ namespace Umbraco.Core.Models // internal - must be invoked by the content item // does *not* validate the value - content item must validate first - internal void PublishValue(int? languageId = null, string segment = null) + internal void PublishValue(string culture = null, string segment = null) { - PropertyType.ValidateVariation(languageId, segment, true); + PropertyType.ValidateVariation(culture, segment, true); - (var pvalue, _) = GetPValue(languageId, segment, false); + (var pvalue, _) = GetPValue(culture, segment, false); if (pvalue == null) return; PublishPropertyValue(pvalue); } // internal - must be invoked by the content item // does *not* validate the value - content item must validate first - internal void PublishCultureValues(int? languageId = null) + internal void PublishCultureValues(string culture = null) { // if invariant and invariant-neutral is supported, publish invariant-neutral - if (!languageId.HasValue && PropertyType.ValidateVariation(null, null, false)) + if (culture == null && PropertyType.ValidateVariation(null, null, false)) PublishPropertyValue(_pvalue); // publish everything not invariant-neutral that matches the culture and is supported if (_vvalues != null) { var pvalues = _vvalues - .Where(x => x.Value.LanguageId == languageId) - .Where(x => PropertyType.ValidateVariation(languageId, x.Value.Segment, false)) + .Where(x => x.Value.Culture.InvariantEquals(culture)) + .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) PublishPropertyValue(pvalue); @@ -206,7 +211,7 @@ namespace Umbraco.Core.Models if (_vvalues != null) { var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) ClearPublishedPropertyValue(pvalue); @@ -214,25 +219,25 @@ namespace Umbraco.Core.Models } // internal - must be invoked by the content item - internal void ClearPublishedValue(int? languageId = null, string segment = null) + internal void ClearPublishedValue(string culture = null, string segment = null) { - PropertyType.ValidateVariation(languageId, segment, true); - (var pvalue, _) = GetPValue(languageId, segment, false); + PropertyType.ValidateVariation(culture, segment, true); + (var pvalue, _) = GetPValue(culture, segment, false); if (pvalue == null) return; ClearPublishedPropertyValue(pvalue); } // internal - must be invoked by the content item - internal void ClearPublishedCultureValues(int? languageId = null) + internal void ClearPublishedCultureValues(string culture = null) { - if (!languageId.HasValue && PropertyType.ValidateVariation(null, null, false)) + if (culture == null && PropertyType.ValidateVariation(null, null, false)) ClearPublishedPropertyValue(_pvalue); if (_vvalues != null) { var pvalues = _vvalues - .Where(x => x.Value.LanguageId == languageId) - .Where(x => PropertyType.ValidateVariation(languageId, x.Value.Segment, false)) + .Where(x => x.Value.Culture.InvariantEquals(culture)) + .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) ClearPublishedPropertyValue(pvalue); @@ -264,10 +269,10 @@ namespace Umbraco.Core.Models /// /// Sets a value. /// - public void SetValue(object value, int? languageId = null, string segment = null) + public void SetValue(object value, string culture = null, string segment = null) { - PropertyType.ValidateVariation(languageId, segment, true); - (var pvalue, var change) = GetPValue(languageId, segment, true); + PropertyType.ValidateVariation(culture, segment, true); + (var pvalue, var change) = GetPValue(culture, segment, true); var origValue = pvalue.EditedValue; var setValue = PropertyType.ConvertAssignedValue(value); @@ -278,9 +283,9 @@ namespace Umbraco.Core.Models } // bypasses all changes detection and is the *only* way to set the published value - internal void FactorySetValue(int? languageId, string segment, bool published, object value) + internal void FactorySetValue(string culture, string segment, bool published, object value) { - (var pvalue, _) = GetPValue(languageId, segment, true); + (var pvalue, _) = GetPValue(culture, segment, true); if (published && PropertyType.IsPublishing) pvalue.PublishedValue = value; @@ -301,24 +306,24 @@ namespace Umbraco.Core.Models return (_pvalue, change); } - private (PropertyValue, bool) GetPValue(int? languageId, string segment, bool create) + private (PropertyValue, bool) GetPValue(string culture, string segment, bool create) { - if (!languageId.HasValue && segment == null) + if (culture == null && segment == null) return GetPValue(create); var change = false; if (_vvalues == null) { if (!create) return (null, false); - _vvalues = new Dictionary(); + _vvalues = new Dictionary(); change = true; } - var k = new CompositeIntStringKey(languageId, segment); + var k = new CompositeStringStringKey(culture, segment); if (!_vvalues.TryGetValue(k, out var pvalue)) { if (!create) return (null, false); pvalue = _vvalues[k] = new PropertyValue(); - pvalue.LanguageId = languageId; + pvalue.Culture = culture; pvalue.Segment = segment; _values.Add(pvalue); change = true; @@ -343,7 +348,7 @@ namespace Umbraco.Core.Models if (_vvalues == null) return true; var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) .Select(x => x.Value) .ToArray(); @@ -354,11 +359,11 @@ namespace Umbraco.Core.Models /// Gets a value indicating whether the culture/any values are valid. /// /// An invalid value can be saved, but only valid values can be published. - public bool IsCultureValid(int? languageId) + public bool IsCultureValid(string culture) { // culture-neutral is supported, validate culture-neutral // includes mandatory validation - if (PropertyType.ValidateVariation(languageId, null, false) && !IsValidValue(GetValue(languageId))) + if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture))) return false; // either culture-neutral is not supported, or it is valid @@ -368,8 +373,8 @@ namespace Umbraco.Core.Models if (_vvalues == null) return true; var pvalues = _vvalues - .Where(x => x.Value.LanguageId == languageId) - .Where(x => PropertyType.ValidateVariation(languageId, x.Value.Segment, false)) + .Where(x => x.Value.Culture.InvariantEquals(culture)) + .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) .Select(x => x.Value) .ToArray(); @@ -380,10 +385,10 @@ namespace Umbraco.Core.Models /// 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(int? languageId = null, string segment = null) + public bool IsValid(string culture = null, string segment = null) { // single value -> validates mandatory - return IsValidValue(GetValue(languageId, segment)); + return IsValidValue(GetValue(culture, segment)); } /// diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 8c5e318719..b34eca7c57 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -226,10 +226,10 @@ namespace Umbraco.Core.Models /// /// Validates that a variation is valid for the property type. /// - public bool ValidateVariation(int? languageId, string segment, bool throwIfInvalid) + public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) { ContentVariation variation; - if (languageId.HasValue) + if (culture != null) { variation = segment != null ? ContentVariation.CultureSegment diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs index 24c654604c..9d2cca3e6d 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs @@ -21,7 +21,7 @@ /// Other caches that get their raw value from the database would consider that a property has "no /// value" if it is missing, null, or an empty string (including whitespace-only). /// - bool HasValue(int? languageId = null, string segment = null); + bool HasValue(string culture = null, string segment = null); /// /// Gets the source value of the property. @@ -35,7 +35,7 @@ /// If you're using that value, you're probably wrong, unless you're doing some internal /// Umbraco stuff. /// - object GetSourceValue(int? languageId = null, string segment = null); + object GetSourceValue(string culture = null, string segment = null); /// /// Gets the object value of the property. @@ -45,7 +45,7 @@ /// It can be null, or any type of CLR object. /// It has been fully prepared and processed by the appropriate converter. /// - object GetValue(int? languageId = null, string segment = null); + object GetValue(string culture = null, string segment = null); /// /// Gets the XPath value of the property. @@ -55,6 +55,6 @@ /// It must be either null, or a string, or an XPathNavigator. /// It has been fully prepared and processed by the appropriate converter. /// - object GetXPathValue(int? languageId = null, string segment = null); + object GetXPathValue(string culture = null, string segment = null); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs index 918bdb86e4..7e2a5b5498 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs @@ -53,15 +53,15 @@ namespace Umbraco.Core.Models.PublishedContent public string Alias => PropertyType.Alias; /// - public abstract bool HasValue(int? languageId = null, string segment = null); + public abstract bool HasValue(string culture = null, string segment = null); /// - public abstract object GetSourceValue(int? languageId = null, string segment = null); + public abstract object GetSourceValue(string culture = null, string segment = null); /// - public abstract object GetValue(int? languageId = null, string segment = null); + public abstract object GetValue(string culture = null, string segment = null); /// - public abstract object GetXPathValue(int? languageId = null, string segment = null); + public abstract object GetXPathValue(string culture = null, string segment = null); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index f938880060..e20d8cb49c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -20,20 +20,20 @@ namespace Umbraco.Core.Models.PublishedContent private readonly Lazy _objectValue; private readonly Lazy _xpathValue; - public override object GetSourceValue(int? languageId = null, string segment = null) - => languageId == null & segment == null ? _sourceValue : null; + public override object GetSourceValue(string culture = null, string segment = null) + => culture == null & segment == null ? _sourceValue : null; - public override bool HasValue(int? languageId = null, string segment = null) + public override bool HasValue(string culture = null, string segment = null) { - var sourceValue = GetSourceValue(languageId, segment); + var sourceValue = GetSourceValue(culture, segment); return sourceValue is string s ? !string.IsNullOrWhiteSpace(s) : sourceValue != null; } - public override object GetValue(int? languageId = null, string segment = null) - => languageId == null & segment == null ? _objectValue.Value : null; + public override object GetValue(string culture = null, string segment = null) + => culture == null & segment == null ? _objectValue.Value : null; - public override object GetXPathValue(int? languageId = null, string segment = null) - => languageId == null & segment == null ? _xpathValue.Value : null; + public override object GetXPathValue(string culture = null, string segment = null) + => culture == null & segment == null ? _xpathValue.Value : null; public RawValueProperty(PublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 4e2789bab7..10f2918204 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Factories { internal static class PropertyFactory { - public static IEnumerable BuildEntities(PropertyType[] propertyTypes, IReadOnlyCollection dtos, int publishedVersionId) + public static IEnumerable BuildEntities(PropertyType[] propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) { var properties = new List(); var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable) x); @@ -26,7 +28,7 @@ namespace Umbraco.Core.Persistence.Factories if (xdtos.TryGetValue(propertyType.Id, out var propDtos)) { foreach (var propDto in propDtos) - property.FactorySetValue(propDto.LanguageId, propDto.Segment, propDto.VersionId == publishedVersionId, propDto.Value); + property.FactorySetValue(languageRepository.GetIsoCodeById(propDto.LanguageId), propDto.Segment, propDto.VersionId == publishedVersionId, propDto.Value); } property.ResetDirtyProperties(false); @@ -41,12 +43,12 @@ namespace Umbraco.Core.Persistence.Factories return properties; } - private static PropertyDataDto BuildDto(int versionId, Property property, int? nLanguageId, string segment, object value) + private static PropertyDataDto BuildDto(int versionId, Property property, int? languageId, string segment, object value) { var dto = new PropertyDataDto { VersionId = versionId, PropertyTypeId = property.PropertyTypeId }; - if (nLanguageId.HasValue) - dto.LanguageId = nLanguageId; + if (languageId.HasValue) + dto.LanguageId = languageId; if (segment != null) dto.Segment = segment; @@ -88,7 +90,7 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - public static IEnumerable BuildDtos(int currentVersionId, int publishedVersionId, IEnumerable properties, out bool edited) + public static IEnumerable BuildDtos(int currentVersionId, int publishedVersionId, IEnumerable properties, ILanguageRepository languageRepository, out bool edited) { var propertyDataDtos = new List(); edited = false; @@ -102,15 +104,15 @@ namespace Umbraco.Core.Persistence.Factories { // deal with published value if (propertyValue.PublishedValue != null && publishedVersionId > 0) - propertyDataDtos.Add(BuildDto(publishedVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.PublishedValue)); + propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); // deal with edit value if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.EditedValue)); + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); // deal with missing edit value (fix inconsistencies) else if (propertyValue.PublishedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.PublishedValue)); + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); @@ -123,7 +125,7 @@ namespace Umbraco.Core.Persistence.Factories { // not publishing = only deal with edit values if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.EditedValue)); + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); } edited = true; } diff --git a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs index 36dd10c3fb..26e5255d15 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Repositories ILanguage GetByCultureName(string cultureName); ILanguage GetByIsoCode(string isoCode); - int GetIdByIsoCode(string isoCode); - string GetIsoCodeById(int id); + int? GetIdByIsoCode(string isoCode); + string GetIsoCodeById(int? id); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 84ff426e91..1094d7fb7f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -35,12 +35,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TEntity : class, IUmbracoEntity where TRepository : class, IRepository { - protected ContentRepositoryBase(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger) + protected ContentRepositoryBase(IScopeAccessor scopeAccessor, CacheHelper cache, ILanguageRepository languageRepository, ILogger logger) : base(scopeAccessor, cache, logger) - { } + { + LanguageRepository = languageRepository; + } protected abstract TRepository This { get; } + protected ILanguageRepository LanguageRepository { get; } + protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // fixme inject #region Versions @@ -464,11 +468,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { propertyDataDtos.AddRange(propertyDataDtos1); if (temp.VersionId == temp.PublishedVersionId) // dirty corner case - propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1))); + propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1))); } if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out var propertyDataDtos2)) propertyDataDtos.AddRange(propertyDataDtos2); - var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId).ToList(); + var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList(); // deal with tags foreach (var property in properties) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index cc6247210b..848019cf95 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -25,19 +25,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly IContentTypeRepository _contentTypeRepository; private readonly ITemplateRepository _templateRepository; private readonly ITagRepository _tagRepository; - private readonly ILanguageRepository _languageRepository; private readonly CacheHelper _cacheHelper; private PermissionRepository _permissionRepository; private readonly ContentByGuidReadRepository _contentByGuidReadRepository; private readonly IScopeAccessor _scopeAccessor; public DocumentRepository(IScopeAccessor scopeAccessor, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IContentSection settings) - : base(scopeAccessor, cacheHelper, logger) + : base(scopeAccessor, cacheHelper, languageRepository, logger) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _languageRepository = languageRepository; _cacheHelper = cacheHelper; _scopeAccessor = scopeAccessor; _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, cacheHelper, logger); @@ -317,7 +315,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, content.PublishedVersionId, entity.Properties, out var edited); + var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -462,7 +460,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var deletePropertyDataSql = Sql().Delete().WhereIn(x => x.VersionId, versionToDelete); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, out var edited); + var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, LanguageRepository, out var edited); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -904,12 +902,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (variations.TryGetValue(content.VersionId, out var variation)) foreach (var v in variation) { - content.SetName(v.LanguageId, v.Name); + content.SetName(v.Culture, v.Name); } if (content.PublishedVersionId > 0 && variations.TryGetValue(content.PublishedVersionId, out variation)) foreach (var v in variation) { - content.SetPublishName(v.LanguageId, v.Name); + content.SetPublishName(v.Culture, v.Name); } } @@ -940,7 +938,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement variation.Add(new CultureVariation { - LanguageId = dto.LanguageId, + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Name = dto.Name, Available = dto.Available }); @@ -955,7 +953,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement yield return new ContentVersionCultureVariationDto { VersionId = content.VersionId, - LanguageId = culture, + LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Name = name }; @@ -965,14 +963,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement yield return new ContentVersionCultureVariationDto { VersionId = content.PublishedVersionId, - LanguageId = culture, + LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Name = name }; } private class CultureVariation { - public int LanguageId { get; set; } + public string Culture { get; set; } public string Name { get; set; } public bool Available { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index ae9336593d..f97d2707b2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class LanguageRepository : NPocoRepositoryBase, ILanguageRepository { - private readonly Dictionary _codeIdMap = new Dictionary(); + private readonly Dictionary _codeIdMap = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _idCodeMap = new Dictionary(); public LanguageRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger) @@ -62,7 +62,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var language in languages) { _codeIdMap[language.IsoCode] = language.Id; - _idCodeMap[language.Id] = language.IsoCode; + _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); } } @@ -211,15 +211,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way var id = GetIdByIsoCode(isoCode, throwOnNotFound: false); - return id > 0 ? Get(id) : null; + return id.HasValue ? Get(id.Value) : null; } // fast way of getting an id for an isoCode - avoiding cloning // _codeIdMap is rebuilt whenever PerformGetAll runs - public int GetIdByIsoCode(string isoCode) => GetIdByIsoCode(isoCode, throwOnNotFound: true); + public int? GetIdByIsoCode(string isoCode) => GetIdByIsoCode(isoCode, throwOnNotFound: true); - private int GetIdByIsoCode(string isoCode, bool throwOnNotFound) + private int? GetIdByIsoCode(string isoCode, bool throwOnNotFound) { + if (isoCode == null) return null; + TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way lock (_codeIdMap) { @@ -232,14 +234,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // fast way of getting an isoCode for an id - avoiding cloning // _idCodeMap is rebuilt whenever PerformGetAll runs - public string GetIsoCodeById(int id) => GetIsoCodeById(id, throwOnNotFound: true); + public string GetIsoCodeById(int? id) => GetIsoCodeById(id, throwOnNotFound: true); - private string GetIsoCodeById(int id, bool throwOnNotFound) + private string GetIsoCodeById(int? id, bool throwOnNotFound) { + if (id == null) return null; + TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way lock (_codeIdMap) // yes, we want to lock _codeIdMap { - if (_idCodeMap.TryGetValue(id, out var isoCode)) return isoCode; + if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) return isoCode; } if (throwOnNotFound) throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index a08ecef98d..dfb30a9cb3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -25,8 +25,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; - public MediaRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(scopeAccessor, cache, logger) + public MediaRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, languageRepository, logger) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); @@ -279,7 +279,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -336,7 +336,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 67dbf758db..04e5d64b06 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -23,8 +23,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly IMemberGroupRepository _memberGroupRepository; - public MemberRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository) - : base(scopeAccessor, cache, logger) + public MemberRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, languageRepository, logger) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); @@ -306,7 +306,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -371,7 +371,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == member.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index ea95f8708f..f235a95aa8 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -272,16 +272,16 @@ namespace Umbraco.Core.PropertyEditors /// /// /// - /// + /// /// /// /// /// The object returned will automatically be serialized into json notation. For most property editors /// the value returned is probably just a string but in some cases a json structure will be returned. /// - public virtual object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public virtual object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var val = property.GetValue(languageId, segment); + var val = property.GetValue(culture, segment); if (val == null) return string.Empty; switch (ValueTypes.ToStorageType(ValueType)) @@ -343,12 +343,8 @@ namespace Umbraco.Core.PropertyEditors continue; var xElement = new XElement(nodeName); - if (pvalue.LanguageId.HasValue) - { - var language = localizationService.GetLanguageById(pvalue.LanguageId.Value); - if (language == null) continue; // uh? - xElement.Add(new XAttribute("lang", language.IsoCode)); - } + if (pvalue.Culture != null) + xElement.Add(new XAttribute("lang", pvalue.Culture)); if (pvalue.Segment != null) xElement.Add(new XAttribute("segment", pvalue.Segment)); diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs index 9e31f94121..b5ed7c5917 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs @@ -61,7 +61,7 @@ namespace Umbraco.Core.PropertyEditors /// /// Converts a property value to a value for the editor. /// - object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null); + object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null); // fixme - editing - document or remove these // why property vs propertyType? services should be injected! etc... diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index cb56b39e2c..abbab0ef39 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -354,7 +354,7 @@ namespace Umbraco.Core.Services /// /// Saves and publishes a document branch. /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, int? languageId = null, string segment = null, int userId = 0); + IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = null, string segment = null, int userId = 0); /// /// Saves and publishes a document branch. diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs index d61ee5e310..cda28d2818 100644 --- a/src/Umbraco.Core/Services/ILocalizationService.cs +++ b/src/Umbraco.Core/Services/ILocalizationService.cs @@ -120,7 +120,7 @@ namespace Umbraco.Core.Services /// /// Gets a language identifier by its iso code. /// - int GetLanguageIdByIsoCode(string isoCode); + int? GetLanguageIdByIsoCode(string isoCode); /// /// Gets all available languages diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index bfa6c5916d..84e616d4d7 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1103,14 +1103,14 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable SaveAndPublishBranch(IContent content, bool force, int? languageId = null, string segment = null, int userId = 0) + public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = null, string segment = null, int userId = 0) { segment = segment?.ToLowerInvariant(); - bool IsEditing(IContent c, int? l, string s) - => c.Properties.Any(x => x.Values.Where(y => y.LanguageId == l && y.Segment == s).Any(y => y.EditedValue != y.PublishedValue)); + bool IsEditing(IContent c, string l, string s) + => c.Properties.Any(x => x.Values.Where(y => y.Culture == l && y.Segment == s).Any(y => y.EditedValue != y.PublishedValue)); - return SaveAndPublishBranch(content, force, document => IsEditing(document, languageId, segment), document => document.PublishValues(languageId, segment), userId); + return SaveAndPublishBranch(content, force, document => IsEditing(document, culture, segment), document => document.PublishValues(culture, segment), userId); } /// diff --git a/src/Umbraco.Core/Services/Implement/LocalizationService.cs b/src/Umbraco.Core/Services/Implement/LocalizationService.cs index bf01605416..104268f5e8 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizationService.cs @@ -317,7 +317,7 @@ namespace Umbraco.Core.Services.Implement } /// - public int GetLanguageIdByIsoCode(string isoCode) + public int? GetLanguageIdByIsoCode(string isoCode) { using (ScopeProvider.CreateScope(autoComplete: true)) { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 77b78f355d..7549d4ba2b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -137,6 +137,7 @@ + diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 8965d60018..6fba071709 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -286,7 +286,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(42, types.Count()); + Assert.AreEqual(43, types.Count()); } [Test] diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 3b293621d5..70e3d574b3 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -65,6 +65,8 @@ namespace Umbraco.Tests.Models var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop" }; var prop = new Property(propertyType); + const string langFr = "fr-FR"; + // can set value // and get edited and published value // because non-publishing @@ -84,8 +86,8 @@ namespace Umbraco.Tests.Models Assert.IsNull(prop.GetValue(published: true)); // cannot set non-supported variation value - Assert.Throws(() => prop.SetValue("x", 1)); - Assert.IsNull(prop.GetValue(1)); + Assert.Throws(() => prop.SetValue("x", langFr)); + Assert.IsNull(prop.GetValue(langFr)); // can publish value // and get edited and published values @@ -109,41 +111,41 @@ namespace Umbraco.Tests.Models // can set value // and get values - prop.SetValue("c", 1); + prop.SetValue("c", langFr); Assert.AreEqual("b", prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); - Assert.AreEqual("c", prop.GetValue(1)); - Assert.IsNull(prop.GetValue(1, 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(1); + prop.PublishValue(langFr); Assert.AreEqual("b", prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); - Assert.AreEqual("c", prop.GetValue(1)); - Assert.AreEqual("c", prop.GetValue(1, 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()); Assert.IsNull(prop.GetValue(published: true)); - Assert.AreEqual("c", prop.GetValue(1)); - Assert.IsNull(prop.GetValue(1, 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)); - Assert.AreEqual("c", prop.GetValue(1)); - Assert.AreEqual("c", prop.GetValue(1, published: true)); + Assert.AreEqual("c", prop.GetValue(langFr)); + Assert.AreEqual("c", prop.GetValue(langFr, published: true)); // same for culture - prop.ClearPublishedCultureValues(1); - Assert.AreEqual("c", prop.GetValue(1)); - Assert.IsNull(prop.GetValue(1, published: true)); - prop.PublishCultureValues(1); - Assert.AreEqual("c", prop.GetValue(1)); - Assert.AreEqual("c", prop.GetValue(1, published: true)); + prop.ClearPublishedCultureValues(langFr); + Assert.AreEqual("c", prop.GetValue(langFr)); + Assert.IsNull(prop.GetValue(langFr, published: true)); + prop.PublishCultureValues(langFr); + Assert.AreEqual("c", prop.GetValue(langFr)); + Assert.AreEqual("c", prop.GetValue(langFr, published: true)); prop.ClearPublishedCultureValues(); Assert.AreEqual("b", prop.GetValue()); @@ -159,8 +161,8 @@ namespace Umbraco.Tests.Models var contentType = new ContentType(-1) { Alias = "contentType" }; var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; - const int langFr = 1; - const int langUk = 2; + const string langFr = "fr-FR"; + const string langUk = "en-UK"; Assert.Throws(() => content.SetName(langFr, "name-fr")); @@ -188,6 +190,8 @@ namespace Umbraco.Tests.Models [Test] public void ContentTests() { + const string langFr = "fr-FR"; + var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop" }; var contentType = new ContentType(-1) { Alias = "contentType" }; contentType.AddPropertyType(propertyType); @@ -202,8 +206,8 @@ namespace Umbraco.Tests.Models Assert.IsNull(content.GetValue("prop", published: true)); // cannot set non-supported variation value - Assert.Throws(() => content.SetValue("prop", "x", 1)); - Assert.IsNull(content.GetValue("prop", 1)); + Assert.Throws(() => content.SetValue("prop", "x", langFr)); + Assert.IsNull(content.GetValue("prop", langFr)); // can publish value // and get edited and published values @@ -228,41 +232,43 @@ namespace Umbraco.Tests.Models // can set value // and get values - content.SetValue("prop", "c", 1); + content.SetValue("prop", "c", langFr); Assert.AreEqual("b", content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - Assert.AreEqual("c", content.GetValue("prop", 1)); - Assert.IsNull(content.GetValue("prop", 1, published: true)); + Assert.AreEqual("c", content.GetValue("prop", langFr)); + Assert.IsNull(content.GetValue("prop", langFr, published: true)); // can publish value // and get edited and published values - content.PublishValues(1); + Assert.Throws(() => content.PublishValues(langFr)); // no name + content.SetName(langFr, "name-fr"); + content.PublishValues(langFr); Assert.AreEqual("b", content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - Assert.AreEqual("c", content.GetValue("prop", 1)); - Assert.AreEqual("c", content.GetValue("prop", 1, 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")); Assert.IsNull(content.GetValue("prop", published: true)); - Assert.AreEqual("c", content.GetValue("prop", 1)); - Assert.IsNull(content.GetValue("prop", 1, published: true)); + Assert.AreEqual("c", content.GetValue("prop", langFr)); + Assert.IsNull(content.GetValue("prop", langFr, published: true)); // can publish all content.PublishAllValues(); Assert.AreEqual("b", content.GetValue("prop")); Assert.AreEqual("b", content.GetValue("prop", published: true)); - Assert.AreEqual("c", content.GetValue("prop", 1)); - Assert.AreEqual("c", content.GetValue("prop", 1, published: true)); + Assert.AreEqual("c", content.GetValue("prop", langFr)); + Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // same for culture - content.ClearCulturePublishedValues(1); - Assert.AreEqual("c", content.GetValue("prop", 1)); - Assert.IsNull(content.GetValue("prop", 1, published: true)); - content.PublishCultureValues(1); - Assert.AreEqual("c", content.GetValue("prop", 1)); - Assert.AreEqual("c", content.GetValue("prop", 1, published: true)); + content.ClearCulturePublishedValues(langFr); + Assert.AreEqual("c", content.GetValue("prop", langFr)); + Assert.IsNull(content.GetValue("prop", langFr, published: true)); + content.PublishCultureValues(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")); @@ -273,21 +279,21 @@ namespace Umbraco.Tests.Models var other = new Content("other", -1, contentType) { Id = 2, VersionId = 1 }; other.SetValue("prop", "o"); - other.SetValue("prop", "o1", 1); + 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.AreEqual("o1", content.GetValue("prop", 1)); - Assert.AreEqual("c", content.GetValue("prop", 1, 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.AreEqual("c", content.GetValue("prop", 1)); - Assert.AreEqual("c", content.GetValue("prop", 1, published: true)); + Assert.AreEqual("c", content.GetValue("prop", langFr)); + Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 2e72a2e14c..53985b7897 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Persistence.Repositories mediaTypeRepository = new MediaTypeRepository(scopeAccessor, cacheHelper, Logger); var tagRepository = new TagRepository(scopeAccessor, cacheHelper, Logger); - var repository = new MediaRepository(scopeAccessor, cacheHelper, Logger, mediaTypeRepository, tagRepository, Mock.Of()); + var repository = new MediaRepository(scopeAccessor, cacheHelper, Logger, mediaTypeRepository, tagRepository, Mock.Of(), Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 4f901935dc..d07d2dda9a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -31,7 +31,7 @@ namespace Umbraco.Tests.Persistence.Repositories memberTypeRepository = new MemberTypeRepository(accessor, DisabledCache, Logger); memberGroupRepository = new MemberGroupRepository(accessor, DisabledCache, Logger); var tagRepo = new TagRepository(accessor, DisabledCache, Logger); - var repository = new MemberRepository(accessor, DisabledCache, Logger, memberTypeRepository, memberGroupRepository, tagRepo); + var repository = new MemberRepository(accessor, DisabledCache, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index 216fb08ecd..90230e15c7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; @@ -964,7 +965,7 @@ namespace Umbraco.Tests.Persistence.Repositories var accessor = (IScopeAccessor) provider; var tagRepository = new TagRepository(accessor, DisabledCache, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, DisabledCache, Logger); - var repository = new MediaRepository(accessor, DisabledCache, Logger, mediaTypeRepository, tagRepository, Mock.Of()); + var repository = new MediaRepository(accessor, DisabledCache, Logger, mediaTypeRepository, tagRepository, Mock.Of(), Mock.Of()); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 56d5bfbc0c..2688629c5c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -26,7 +26,7 @@ namespace Umbraco.Tests.Persistence.Repositories var accessor = (IScopeAccessor) provider; mediaTypeRepository = new MediaTypeRepository(accessor, CacheHelper, Mock.Of()); var tagRepository = new TagRepository(accessor, CacheHelper, Mock.Of()); - var repository = new MediaRepository(accessor, CacheHelper, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of()); + var repository = new MediaRepository(accessor, CacheHelper, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index a916a2d51e..22e110dd20 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -242,10 +242,10 @@ namespace Umbraco.Tests.Published _owner = owner; } - public override bool HasValue(int? languageId = null, string segment = null) => _hasValue; - public override object GetSourceValue(int? languageId = null, string segment = null) => _sourceValue; - public override object GetValue(int? languageId = null, string segment = null) => PropertyType.ConvertInterToObject(_owner, ReferenceCacheLevel, InterValue, _preview); - public override object GetXPathValue(int? languageId = null, string segment = null) => throw new WontImplementException(); + public override bool HasValue(string culture = null, string segment = null) => _hasValue; + public override object GetSourceValue(string culture = null, string segment = null) => _sourceValue; + public override object GetValue(string culture = null, string segment = null) => PropertyType.ConvertInterToObject(_owner, ReferenceCacheLevel, InterValue, _preview); + public override object GetXPathValue(string culture = null, string segment = null) => throw new WontImplementException(); } class TestPublishedContent : PublishedContentBase diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs index 8f6ee5c2b1..7a6c684eb4 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs @@ -256,10 +256,10 @@ namespace Umbraco.Tests.PublishedContent public bool SolidHasValue { get; set; } public object SolidXPathValue { get; set; } - public object GetSourceValue(int? languageId = null, string segment = null) => SolidSourceValue; - public object GetValue(int? languageId = null, string segment = null) => SolidValue; - public object GetXPathValue(int? languageId = null, string segment = null) => SolidXPathValue; - public bool HasValue(int? languageId = null, string segment = null) => SolidHasValue; + public object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue; + public object GetValue(string culture = null, string segment = null) => SolidValue; + public object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue; + public bool HasValue(string culture = null, string segment = null) => SolidHasValue; } [PublishedModel("ContentType2")] diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 0e726064f4..04ed54d81c 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -356,7 +356,7 @@ namespace Umbraco.Tests.PublishedContent var result = doc.Ancestors().OrderBy(x => x.Level) .Single() .Descendants() - .FirstOrDefault(x => x.Value("selectedNodes", "").Split(',').Contains("1173")); + .FirstOrDefault(x => x.Value("selectedNodes", defaultValue: "").Split(',').Contains("1173")); Assert.IsNotNull(result); } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 79929ff29d..b3bdf4f1ba 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2506,200 +2506,200 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var content = contentService.Create("Home US", - 1, "umbTextpage"); - // act + // act - content.SetValue("author", "Barack Obama"); - content.SetValue("prop", "value-fr1", langFr.Id); - content.SetValue("prop", "value-uk1", langUk.Id); - content.SetName(langFr.Id, "name-fr"); - content.SetName(langUk.Id, "name-uk"); - contentService.Save(content); + content.SetValue("author", "Barack Obama"); + content.SetValue("prop", "value-fr1", langFr.IsoCode); + content.SetValue("prop", "value-uk1", langUk.IsoCode); + content.SetName(langFr.IsoCode, "name-fr"); + content.SetName(langUk.IsoCode, "name-uk"); + contentService.Save(content); - // content has been saved, - // it has names, but no publishNames, and no published cultures + // content has been saved, + // it has names, but no publishNames, and no published cultures - var content2 = contentService.GetById(content.Id); + var content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.Id)); - Assert.AreEqual("name-uk", content2.GetName(langUk.Id)); + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.Id)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.Id)); - Assert.IsNull(content2.GetValue("prop", langFr.Id, published: true)); - Assert.IsNull(content2.GetValue("prop", langUk.Id, published: true)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.IsNull(content2.GetValue("prop", langUk.IsoCode, published: true)); - Assert.IsNull(content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.Id)); - Assert.IsNull(content2.GetPublishName(langUk.Id)); + Assert.IsNull(content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.IsNull(content2.GetPublishName(langUk.IsoCode)); - Assert.IsTrue(content.IsCultureAvailable(langFr.Id)); - Assert.IsTrue(content.IsCultureAvailable(langUk.Id)); - Assert.IsFalse(content.IsCultureAvailable(langDe.Id)); + Assert.IsTrue(content.IsCultureAvailable(langFr.IsoCode)); + Assert.IsTrue(content.IsCultureAvailable(langUk.IsoCode)); + Assert.IsFalse(content.IsCultureAvailable(langDe.IsoCode)); - Assert.IsFalse(content.IsCulturePublished(langFr.Id)); - Assert.IsFalse(content.IsCulturePublished(langUk.Id)); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); - // act + // act - content.PublishValues(langFr.Id); - content.PublishValues(langUk.Id); - contentService.SaveAndPublish(content); + content.PublishValues(langFr.IsoCode); + content.PublishValues(langUk.IsoCode); + contentService.SaveAndPublish(content); - // both FR and UK have been published, - // and content has been published, - // it has names, publishNames, and published cultures + // both FR and UK have been published, + // and content has been published, + // it has names, publishNames, and published cultures - content2 = contentService.GetById(content.Id); + content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.Id)); - Assert.AreEqual("name-uk", content2.GetName(langUk.Id)); + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); - Assert.IsNull(content2.PublishName); // we haven't published InvariantNeutral - Assert.AreEqual("name-fr", content2.GetPublishName(langFr.Id)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.Id)); + Assert.IsNull(content2.PublishName); // we haven't published InvariantNeutral + Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.Id)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.Id)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.Id, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.Id, published: true)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - Assert.IsTrue(content.IsCulturePublished(langFr.Id)); - Assert.IsTrue(content.IsCulturePublished(langUk.Id)); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - // act + // act - content.PublishValues(); - contentService.SaveAndPublish(content); + content.PublishValues(); + contentService.SaveAndPublish(content); - // now it has publish name for invariant neutral + // now it has publish name for invariant neutral - content2 = contentService.GetById(content.Id); + content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("Home US", content2.PublishName); - // act + // act - content.SetName(null, "Home US2"); - content.SetName(langFr.Id, "name-fr2"); - content.SetName(langUk.Id, "name-uk2"); - content.SetValue("author", "Barack Obama2"); - content.SetValue("prop", "value-fr2", langFr.Id); - content.SetValue("prop", "value-uk2", langUk.Id); - contentService.Save(content); + content.SetName(null, "Home US2"); + content.SetName(langFr.IsoCode, "name-fr2"); + content.SetName(langUk.IsoCode, "name-uk2"); + content.SetValue("author", "Barack Obama2"); + content.SetValue("prop", "value-fr2", langFr.IsoCode); + content.SetValue("prop", "value-uk2", langUk.IsoCode); + contentService.Save(content); - // content has been saved, - // it has updated names, unchanged publishNames, and published cultures + // content has been saved, + // it has updated names, unchanged publishNames, and published cultures - content2 = contentService.GetById(content.Id); + content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.Id)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.Id)); + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); - Assert.AreEqual("name-fr", content2.GetPublishName(langFr.Id)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.Id)); + Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - Assert.AreEqual("Barack Obama2", content2.GetValue("author")); - Assert.AreEqual("Barack Obama", content2.GetValue("author", published: true)); + Assert.AreEqual("Barack Obama2", content2.GetValue("author")); + Assert.AreEqual("Barack Obama", content2.GetValue("author", published: true)); - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.Id)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.Id)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.Id, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.Id, published: true)); + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - Assert.IsTrue(content.IsCulturePublished(langFr.Id)); - Assert.IsTrue(content.IsCulturePublished(langUk.Id)); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - // act - // cannot just 'save' since we are changing what's published! + // act + // cannot just 'save' since we are changing what's published! - content.ClearPublishedValues(langFr.Id); - contentService.SaveAndPublish(content); + content.ClearPublishedValues(langFr.IsoCode); + contentService.SaveAndPublish(content); - // content has been published, - // the french culture is gone + // content has been published, + // the french culture is gone - content2 = contentService.GetById(content.Id); + content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.Id)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.Id)); + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.Id)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.Id)); + Assert.AreEqual("Home US", content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.Id)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.Id)); - Assert.IsNull(content2.GetValue("prop", langFr.Id, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.Id, published: true)); + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - Assert.IsFalse(content.IsCulturePublished(langFr.Id)); - Assert.IsTrue(content.IsCulturePublished(langUk.Id)); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - // act + // act - contentService.Unpublish(content); + contentService.Unpublish(content); - // content has been unpublished, - // but properties, names, etc. retain their 'published' values so the content - // can be re-published in its exact original state (before being unpublished) - // - // BEWARE! - // in order for a content to be unpublished as a whole, and then republished in - // its exact previous state, properties and names etc. retain their published - // values even though the content is not published - hence many things being - // non-null or true below - always check against content.Published to be sure + // content has been unpublished, + // but properties, names, etc. retain their 'published' values so the content + // can be re-published in its exact original state (before being unpublished) + // + // BEWARE! + // in order for a content to be unpublished as a whole, and then republished in + // its exact previous state, properties and names etc. retain their published + // values even though the content is not published - hence many things being + // non-null or true below - always check against content.Published to be sure - content2 = contentService.GetById(content.Id); + content2 = contentService.GetById(content.Id); - Assert.IsFalse(content2.Published); + Assert.IsFalse(content2.Published); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.Id)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.Id)); + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); // not null, see note above - Assert.IsNull(content2.GetPublishName(langFr.Id)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.Id)); // not null, see note above + Assert.AreEqual("Home US", content2.PublishName); // not null, see note above + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); // not null, see note above - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.Id)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.Id)); - Assert.IsNull(content2.GetValue("prop", langFr.Id, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.Id, published: true)); // has value, see note above + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); // has value, see note above - Assert.IsFalse(content.IsCulturePublished(langFr.Id)); - Assert.IsTrue(content.IsCulturePublished(langUk.Id)); // still true, see note above + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); // still true, see note above - // act + // act - contentService.SaveAndPublish(content); + contentService.SaveAndPublish(content); - // content has been re-published, - // everything is back to what it was before being unpublished + // content has been re-published, + // everything is back to what it was before being unpublished - content2 = contentService.GetById(content.Id); + content2 = contentService.GetById(content.Id); - Assert.IsTrue(content2.Published); + Assert.IsTrue(content2.Published); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.Id)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.Id)); + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.Id)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.Id)); + Assert.AreEqual("Home US", content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.Id)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.Id)); - Assert.IsNull(content2.GetValue("prop", langFr.Id, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.Id, published: true)); + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - Assert.IsFalse(content.IsCulturePublished(langFr.Id)); - Assert.IsTrue(content.IsCulturePublished(langUk.Id)); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); } private IEnumerable CreateContentHierarchy() diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 47a847ad43..25ac6314e2 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -140,8 +140,8 @@ namespace Umbraco.Tests.TestHelpers { // compare values var actualProperty = (Property) actual; - var expectedPropertyValues = expectedProperty.Values.OrderBy(x => x.LanguageId).ThenBy(x => x.Segment).ToArray(); - var actualPropertyValues = actualProperty.Values.OrderBy(x => x.LanguageId).ThenBy(x => x.Segment).ToArray(); + var expectedPropertyValues = expectedProperty.Values.OrderBy(x => x.Culture).ThenBy(x => x.Segment).ToArray(); + var actualPropertyValues = actualProperty.Values.OrderBy(x => x.Culture).ThenBy(x => x.Segment).ToArray(); if (expectedPropertyValues.Length != actualPropertyValues.Length) Assert.Fail($"{property.DeclaringType.Name}.{property.Name}: Expected {expectedPropertyValues.Length} but got {actualPropertyValues.Length}."); for (var i = 0; i < expectedPropertyValues.Length; i++) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index dce9ca531d..6eb8db3c61 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -265,8 +265,8 @@ namespace Umbraco.Web.Editors HandleContentNotFound(id); return null;//irrelevant since the above throws } - - var content = MapToDisplay(foundContent, languageId); + + var content = MapToDisplay(foundContent, GetLanguageCulture(languageId)); return content; } @@ -606,7 +606,7 @@ namespace Umbraco.Web.Editors { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the modelstate to the outgoing object and throw a validation message - var forDisplay = MapToDisplay(contentItem.PersistedContent, contentItem.LanguageId); + var forDisplay = MapToDisplay(contentItem.PersistedContent, GetLanguageCulture(contentItem.LanguageId)); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); @@ -643,14 +643,14 @@ namespace Umbraco.Web.Editors else { //publish the item and check if it worked, if not we will show a diff msg below - contentItem.PersistedContent.PublishValues(contentItem.LanguageId); //we are not checking for a return value here because we've alraedy pre-validated the property values + contentItem.PersistedContent.PublishValues(GetLanguageCulture(contentItem.LanguageId)); //we are not checking for a return value here because we've alraedy pre-validated the property values //check if we are publishing other variants and validate them var allLangs = Services.LocalizationService.GetAllLanguages().ToList(); var variantsToValidate = contentItem.PublishVariations.Where(x => x.LanguageId != contentItem.LanguageId).ToList(); foreach (var publishVariation in variantsToValidate) { - if (!contentItem.PersistedContent.PublishValues(publishVariation.LanguageId)) + if (!contentItem.PersistedContent.PublishValues(GetLanguageCulture(publishVariation.LanguageId))) { var errMsg = Services.TextService.Localize("speechBubbles/contentLangValidationError", new[]{allLangs.First(x => x.Id == publishVariation.LanguageId).CultureName}); ModelState.AddModelError("publish_variant_" + publishVariation.LanguageId + "_", errMsg); @@ -664,7 +664,7 @@ namespace Umbraco.Web.Editors .Where(x => x.Mandatory); foreach (var lang in mandatoryLangs) { - if (contentItem.PersistedContent.Validate(lang.Id).Length > 0) + if (contentItem.PersistedContent.Validate(GetLanguageCulture(lang.Id)).Length > 0) { var errMsg = Services.TextService.Localize("speechBubbles/contentReqLangValidationError", new[]{allLangs.First(x => x.Id == lang.Id).CultureName}); ModelState.AddModelError("publish_variant_" + lang.Id + "_", errMsg); @@ -676,7 +676,7 @@ namespace Umbraco.Web.Editors } //get the updated model - var display = MapToDisplay(contentItem.PersistedContent, contentItem.LanguageId); + var display = MapToDisplay(contentItem.PersistedContent, GetLanguageCulture(contentItem.LanguageId)); //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -977,8 +977,8 @@ namespace Umbraco.Web.Editors base.MapPropertyValues( contentItem, - (save, property) => property.GetValue(save.LanguageId), //get prop val - (save, property, v) => property.SetValue(v, save.LanguageId)); //set prop val + (save, property) => property.GetValue(GetLanguageCulture(save.LanguageId)), //get prop val + (save, property, v) => property.SetValue(v, GetLanguageCulture(save.LanguageId))); //set prop val } /// @@ -1169,23 +1169,28 @@ namespace Umbraco.Web.Editors /// Used to map an instance to a and ensuring a language is present if required /// /// - /// + /// /// - private ContentItemDisplay MapToDisplay(IContent content, int? languageId = null) + private ContentItemDisplay MapToDisplay(IContent content, string culture = null) { //a languageId must exist in the mapping context if this content item has any property type that can be varied by language //otherwise the property validation will fail since it's expecting to be get/set with a language ID. If a languageId is not explicitly //sent up, then it means that the user is editing the default variant language. - if (!languageId.HasValue && content.HasPropertyTypeVaryingByCulture()) + if (culture == null && content.HasPropertyTypeVaryingByCulture()) { - languageId = Services.LocalizationService.GetDefaultVariantLanguage().Id; + culture = Services.LocalizationService.GetDefaultVariantLanguage().IsoCode; } var display = ContextMapper.Map(content, UmbracoContext, - new Dictionary { { ContextMapper.LanguageKey, languageId } }); + new Dictionary { { ContextMapper.CultureKey, culture } }); return display; } + private string GetLanguageCulture(int? languageId) + { + if (languageId == null) return null; + return Core.Composing.Current.Services.LocalizationService.GetLanguageById(languageId.Value).IsoCode; // fixme optimize! + } } } diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 4a016a895b..3fe81757a8 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -96,6 +96,5 @@ namespace Umbraco.Web.Models var defaultLanguage = localizationService.GetAllLanguages().FirstOrDefault(); return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); } - } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs index 5f7cc0e052..10fec19b38 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs @@ -46,9 +46,9 @@ namespace Umbraco.Web.Models.Mapping editor = _propertyEditors[Constants.PropertyEditors.Aliases.NoEdit]; } - var languageId = context.GetLanguageId(); + var culture = context.GetCulture(); - if (!languageId.HasValue && property.PropertyType.Variations == ContentVariation.CultureNeutral) + if (culture == null && property.PropertyType.Variations == ContentVariation.CultureNeutral) { //a language Id needs to be set for a property type that can be varried by language throw new InvalidOperationException($"No languageId found in mapping operation when one is required for the culture neutral property type {property.PropertyType.Alias}"); @@ -74,7 +74,7 @@ namespace Umbraco.Web.Models.Mapping } // 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, languageId); + result.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture); return result; } } diff --git a/src/Umbraco.Web/Models/Mapping/ContextMapper.cs b/src/Umbraco.Web/Models/Mapping/ContextMapper.cs index b46cd219d8..5aa8af7668 100644 --- a/src/Umbraco.Web/Models/Mapping/ContextMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContextMapper.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Models.Mapping internal static class ContextMapper { public const string UmbracoContextKey = "ContextMapper.UmbracoContext"; - public const string LanguageKey = "ContextMapper.LanguageId"; + public const string CultureKey = "ContextMapper.Culture"; public static TDestination Map(TSource obj, UmbracoContext umbracoContext) => Mapper.Map(obj, opt => opt.Items[UmbracoContextKey] = umbracoContext); @@ -77,12 +77,12 @@ namespace Umbraco.Web.Models.Mapping /// /// /// - public static int? GetLanguageId(this ResolutionContext resolutionContext) + public static string GetCulture(this ResolutionContext resolutionContext) { - if (!resolutionContext.Options.Items.TryGetValue(LanguageKey, out var obj)) return null; + if (!resolutionContext.Options.Items.TryGetValue(CultureKey, out var obj)) return null; - if (obj is int i) - return i; + if (obj is string s) + return s; return null; } diff --git a/src/Umbraco.Web/Models/Mapping/VariationResolver.cs b/src/Umbraco.Web/Models/Mapping/VariationResolver.cs index 9d3de07d77..ece6f87a33 100644 --- a/src/Umbraco.Web/Models/Mapping/VariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/VariationResolver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; @@ -29,19 +30,19 @@ namespace Umbraco.Web.Models.Mapping { Language = x, Mandatory = x.Mandatory, - Name = source.GetName(x.Id), - Exists = source.IsCultureAvailable(x.Id), // segments ?? + Name = source.GetName(x.IsoCode), + Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? PublishedState = source.PublishedState.ToString(), //Segment = ?? We'll need to populate this one day when we support segments }).ToList(); - var langId = context.GetLanguageId(); + var culture = context.GetCulture(); //set the current variant being edited to the one found in the context or the default if nothing matches var foundCurrent = false; foreach (var variant in variants) { - if (langId.HasValue && langId.Value == variant.Language.Id) + if (culture.InvariantEquals(variant.Language.IsoCode)) { variant.IsCurrent = true; foundCurrent = true; diff --git a/src/Umbraco.Web/PropertyEditors/DateValueEditor.cs b/src/Umbraco.Web/PropertyEditors/DateValueEditor.cs index 31425fdadd..a03ad4aa00 100644 --- a/src/Umbraco.Web/PropertyEditors/DateValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DateValueEditor.cs @@ -18,9 +18,9 @@ namespace Umbraco.Web.PropertyEditors Validators.Add(new DateTimeValidator()); } - public override object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture= null, string segment = null) { - var date = property.GetValue(languageId, segment).TryConvertTo(); + var date = property.GetValue(culture, segment).TryConvertTo(); if (date.Success == false || date.Result == null) { return String.Empty; diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index e969d2e45b..0d5f6efaf5 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -93,11 +93,11 @@ namespace Umbraco.Web.PropertyEditors //copy each of the property values (variants, segments) to the destination foreach (var propertyValue in property.Values) { - var propVal = property.GetValue(propertyValue.LanguageId, propertyValue.Segment); + var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment); if (propVal == null || !(propVal is string str) || str.IsNullOrWhiteSpace()) continue; var sourcePath = _mediaFileSystem.GetRelativePath(str); var copyPath = _mediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); - args.Copy.SetValue(property.Alias, _mediaFileSystem.GetUrl(copyPath), propertyValue.LanguageId, propertyValue.Segment); + args.Copy.SetValue(property.Alias, _mediaFileSystem.GetUrl(copyPath), propertyValue.Culture, propertyValue.Segment); isUpdated = true; } } @@ -153,11 +153,11 @@ namespace Umbraco.Web.PropertyEditors foreach (var pvalue in property.Values) { - var svalue = property.GetValue(pvalue.LanguageId, pvalue.Segment) as string; + var svalue = property.GetValue(pvalue.Culture, pvalue.Segment) as string; if (string.IsNullOrWhiteSpace(svalue)) - _mediaFileSystem.UploadAutoFillProperties.Reset(model, autoFillConfig, pvalue.LanguageId, pvalue.Segment); + _mediaFileSystem.UploadAutoFillProperties.Reset(model, autoFillConfig, pvalue.Culture, pvalue.Segment); else - _mediaFileSystem.UploadAutoFillProperties.Populate(model, autoFillConfig, _mediaFileSystem.GetRelativePath(svalue), pvalue.LanguageId, pvalue.Segment); + _mediaFileSystem.UploadAutoFillProperties.Populate(model, autoFillConfig, _mediaFileSystem.GetRelativePath(svalue), pvalue.Culture, pvalue.Segment); } } } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index 6d3c5b4f61..6183641aaf 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -148,13 +148,13 @@ namespace Umbraco.Web.PropertyEditors //copy each of the property values (variants, segments) to the destination by using the edited value foreach (var propertyValue in property.Values) { - var propVal = property.GetValue(propertyValue.LanguageId, propertyValue.Segment); + var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment); var src = GetFileSrcFromPropertyValue(propVal, out var jo); if (src == null) continue; var sourcePath = _mediaFileSystem.GetRelativePath(src); var copyPath = _mediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); jo["src"] = _mediaFileSystem.GetUrl(copyPath); - args.Copy.SetValue(property.Alias, jo.ToString(), propertyValue.LanguageId, propertyValue.Segment); + args.Copy.SetValue(property.Alias, jo.ToString(), propertyValue.Culture, propertyValue.Segment); isUpdated = true; } } @@ -209,10 +209,10 @@ namespace Umbraco.Web.PropertyEditors foreach (var pvalue in property.Values) { - var svalue = property.GetValue(pvalue.LanguageId, pvalue.Segment) as string; + var svalue = property.GetValue(pvalue.Culture, pvalue.Segment) as string; if (string.IsNullOrWhiteSpace(svalue)) { - _autoFillProperties.Reset(model, autoFillConfig, pvalue.LanguageId, pvalue.Segment); + _autoFillProperties.Reset(model, autoFillConfig, pvalue.Culture, pvalue.Segment); continue; } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index bce989fb18..98e8346441 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -32,9 +32,9 @@ namespace Umbraco.Web.PropertyEditors /// This is called to merge in the prevalue crops with the value that is saved - similar to the property value converter for the front-end /// - public override object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var val = property.GetValue(languageId, segment); + var val = property.GetValue(culture, segment); if (val == null) return null; ImageCropperValue value; diff --git a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs index a6373bd87d..645296a354 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -85,9 +85,9 @@ namespace Umbraco.Web.PropertyEditors /// /// The legacy property editor saved this data as new line delimited! strange but we have to maintain that. /// - public override object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var val = property.GetValue(languageId, segment); + var val = property.GetValue(culture, segment); return val?.ToString().Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries) .Select(x => JObject.FromObject(new {value = x})) ?? new JObject[] { }; diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 52933e5846..4cb8fde97a 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -139,9 +139,9 @@ namespace Umbraco.Web.PropertyEditors // note: there is NO variant support here - public override object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var val = property.GetValue(languageId, segment); + var val = property.GetValue(culture, segment); if (val == null || string.IsNullOrWhiteSpace(val.ToString())) return string.Empty; diff --git a/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs b/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs index 6cd9675f67..25a1c25284 100644 --- a/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs @@ -70,12 +70,12 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - /// + /// /// /// - public override object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var delimited = base.ToEditor(property, dataTypeService, languageId, segment).ToString(); + var delimited = base.ToEditor(property, dataTypeService, culture, segment).ToString(); return delimited.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 181471b315..797d0eaf59 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -61,12 +61,12 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - /// + /// /// /// - public override object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var val = property.GetValue(languageId, segment); + var val = property.GetValue(culture, segment); if (val == null) return null; diff --git a/src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs index f860a5abe7..754cef1f31 100644 --- a/src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs @@ -20,15 +20,15 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - /// + /// /// /// /// /// The object returned will always be a string and if the database type is not a valid string type an exception is thrown /// - public override object ToEditor(Property property, IDataTypeService dataTypeService, int? languageId = null, string segment = null) + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var val = property.GetValue(languageId, segment); + var val = property.GetValue(culture, segment); if (val == null) return string.Empty; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs index 6fd731cbee..30bb553ae2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs @@ -159,7 +159,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource pdatas.Add(pdata); var type = PrimitiveSerializer.Char.ReadFrom(stream); - pdata.LanguageId = (int?) ReadObject(type, stream); + pdata.Culture = (string) ReadObject(type, stream); type = PrimitiveSerializer.Char.ReadFrom(stream); pdata.Segment = (string) ReadObject(type, stream); type = PrimitiveSerializer.Char.ReadFrom(stream); @@ -211,7 +211,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // write each value foreach (var pdata in kvp.Value) { - WriteObject(pdata.LanguageId, stream); + WriteObject(pdata.Culture, stream); WriteObject(pdata.Segment, stream); WriteObject(pdata.Value, stream); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs index ab9d3cdcac..ddb9607575 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs @@ -4,8 +4,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { internal class PropertyData { - [JsonProperty("lang")] - public int? LanguageId { get; set; } + [JsonProperty("culture")] + public string Culture { get; set; } [JsonProperty("seg")] public string Segment { get; set; } @@ -13,4 +13,4 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource [JsonProperty("val")] public object Value { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 9a74b66897..7fe898b080 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private object _interValue; // the variant source and inter values - private Dictionary _sourceValues; + private Dictionary _sourceValues; // the variant and non-variant object values private CacheValues _cacheValues; @@ -49,16 +49,16 @@ namespace Umbraco.Web.PublishedCache.NuCache { foreach (var sourceValue in sourceValues) { - if (sourceValue.LanguageId == null && sourceValue.Segment == null) + if (sourceValue.Culture == null && sourceValue.Segment == null) { _sourceValue = sourceValue.Value; } else { if (_sourceValues == null) - _sourceValues = new Dictionary(); - _sourceValues[new CompositeIntStringKey(sourceValue.LanguageId, sourceValue.Segment)] - = new SourceInterValue { LanguageId = sourceValue.LanguageId, Segment = sourceValue.Segment, SourceValue = sourceValue.Value }; + _sourceValues = new Dictionary(); + _sourceValues[new CompositeStringStringKey(sourceValue.Culture, sourceValue.Segment)] + = new SourceInterValue { Culture = sourceValue.Culture, Segment = sourceValue.Segment, SourceValue = sourceValue.Value }; } } } @@ -84,7 +84,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; } - public override bool HasValue(int? languageId = null, string segment = null) => _sourceValue != null + public override bool HasValue(string culture = null, string segment = null) => _sourceValue != null && (!(_sourceValue is string) || string.IsNullOrWhiteSpace((string) _sourceValue) == false); // used to cache the recursive *property* for this property @@ -143,9 +143,9 @@ namespace Umbraco.Web.PublishedCache.NuCache } // this is always invoked from within a lock, so does not require its own lock - private object GetInterValue(int? languageId, string segment) + private object GetInterValue(string culture, string segment) { - if (languageId == null && segment == null) + if (culture == null && segment == null) { if (_interInitialized) return _interValue; _interValue = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing); @@ -154,11 +154,11 @@ namespace Umbraco.Web.PublishedCache.NuCache } if (_sourceValues == null) - _sourceValues = new Dictionary(); + _sourceValues = new Dictionary(); - var k = new CompositeIntStringKey(languageId, segment); + var k = new CompositeStringStringKey(culture, segment); if (!_sourceValues.TryGetValue(k, out var vvalue)) - _sourceValues[k] = vvalue = new SourceInterValue { LanguageId = languageId, Segment = segment }; + _sourceValues[k] = vvalue = new SourceInterValue { Culture = culture, Segment = segment }; if (vvalue.InterInitialized) return vvalue.InterValue; vvalue.InterValue = PropertyType.ConvertSourceToInter(_content, vvalue.SourceValue, _isPreviewing); @@ -166,45 +166,45 @@ namespace Umbraco.Web.PublishedCache.NuCache return vvalue.InterValue; } - public override object GetSourceValue(int? languageId = null, string segment = null) + public override object GetSourceValue(string culture = null, string segment = null) { - if (languageId == null && segment == null) + if (culture == null && segment == null) return _sourceValue; lock (_locko) { if (_sourceValues == null) return null; - return _sourceValues.TryGetValue(new CompositeIntStringKey(languageId, segment), out var sourceValue) ? sourceValue.SourceValue : null; + return _sourceValues.TryGetValue(new CompositeStringStringKey(culture, segment), out var sourceValue) ? sourceValue.SourceValue : null; } } - public override object GetValue(int? languageId = null, string segment = null) + public override object GetValue(string culture = null, string segment = null) { lock (_locko) { - var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(languageId, segment); + var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); // initial reference cache level always is .Content const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(languageId, segment), _isPreviewing); + cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); cacheValues.ObjectInitialized = true; return cacheValues.ObjectValue; } } - public override object GetXPathValue(int? languageId = null, string segment = null) + public override object GetXPathValue(string culture = null, string segment = null) { lock (_locko) { - var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(languageId, segment); + var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); // initial reference cache level always is .Content const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; if (cacheValues.XPathInitialized) return cacheValues.XPathValue; - cacheValues.XPathValue = PropertyType.ConvertInterToXPath(_content, initialCacheLevel, GetInterValue(languageId, segment), _isPreviewing); + cacheValues.XPathValue = PropertyType.ConvertInterToXPath(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); cacheValues.XPathInitialized = true; return cacheValues.XPathValue; } @@ -222,18 +222,18 @@ namespace Umbraco.Web.PublishedCache.NuCache private class CacheValues : CacheValue { - private Dictionary _values; + private Dictionary _values; // this is always invoked from within a lock, so does not require its own lock - public CacheValue For(int? languageId, string segment) + public CacheValue For(string culture, string segment) { - if (languageId == null && segment == null) + if (culture == null && segment == null) return this; if (_values == null) - _values = new Dictionary(); + _values = new Dictionary(); - var k = new CompositeIntStringKey(languageId, segment); + var k = new CompositeStringStringKey(culture, segment); if (!_values.TryGetValue(k, out var value)) _values[k] = value = new CacheValue(); @@ -243,9 +243,14 @@ namespace Umbraco.Web.PublishedCache.NuCache private class SourceInterValue { + private string _culture; private string _segment; - public int? LanguageId { get; set; } + public string Culture + { + get => _culture; + internal set => _culture = value?.ToLowerInvariant(); + } public string Segment { get => _segment; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 992ac62733..7f26f3c753 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1183,7 +1183,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var value = published ? pvalue.PublishedValue : pvalue.EditedValue; if (value != null) - pdatas.Add(new PropertyData { LanguageId = pvalue.LanguageId, Segment = pvalue.Segment, Value = value }); + pdatas.Add(new PropertyData { Culture = pvalue.Culture, Segment = pvalue.Segment, Value = value }); //Core.Composing.Current.Logger.Debug($"{content.Id} {prop.Alias} [{pvalue.LanguageId},{pvalue.Segment}] {value} {(published?"pub":"edit")}"); diff --git a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs index 2c5b93f219..d8db937ca8 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.PublishedCache IsMember = propertyType.ContentType.ItemType == PublishedItemType.Member; } - public override bool HasValue(int? languageId = null, string segment = null) + public override bool HasValue(string culture = null, string segment = null) => _sourceValue != null && (!(_sourceValue is string s) || !string.IsNullOrWhiteSpace(s)); // used to cache the CacheValues of this property @@ -136,9 +136,9 @@ namespace Umbraco.Web.PublishedCache return _interValue; } - public override object GetSourceValue(int? languageId = null, string segment = null) => _sourceValue; + public override object GetSourceValue(string culture = null, string segment = null) => _sourceValue; - public override object GetValue(int? languageId = null, string segment = null) + public override object GetValue(string culture = null, string segment = null) { GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); @@ -152,7 +152,7 @@ namespace Umbraco.Web.PublishedCache } } - public override object GetXPathValue(int? languageId = null, string segment = null) + public override object GetXPathValue(string culture = null, string segment = null) { GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedProperty.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedProperty.cs index 632698c37e..ea8ab925c6 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedProperty.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedProperty.cs @@ -27,13 +27,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// /// Gets the raw value of the property. /// - public override object GetSourceValue(int? languageId = null, string segment = null) => _sourceValue; + public override object GetSourceValue(string culture = null, string segment = null) => _sourceValue; // in the Xml cache, everything is a string, and to have a value // you want to have a non-null, non-empty string. - public override bool HasValue(int? languageId = null, string segment = null) => _sourceValue.Trim().Length > 0; + public override bool HasValue(string culture = null, string segment = null) => _sourceValue.Trim().Length > 0; - public override object GetValue(int? languageId = null, string segment = null) + public override object GetValue(string culture = null, string segment = null) { // NOT caching the source (intermediate) value since we'll never need it // everything in Xml cache is per-request anyways @@ -48,7 +48,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return _objectValue; } - public override object GetXPathValue(int? languageId = null, string segment = null) { throw new NotImplementedException(); } + public override object GetXPathValue(string culture = null, string segment = null) { throw new NotImplementedException(); } public XmlPublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing, XmlNode propertyXmlData) : this(propertyType, content, isPreviewing) diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedContentPropertyExtension.cs index 3314bc09fc..6d397ffaa3 100644 --- a/src/Umbraco.Web/PublishedContentPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedContentPropertyExtension.cs @@ -10,24 +10,24 @@ namespace Umbraco.Web { #region Value - public static T Value(this IPublishedProperty property, int? languageId = null, string segment = null) + public static T Value(this IPublishedProperty property, string culture = null, string segment = null) { - return property.Value(false, default(T), languageId, segment); + return property.Value(false, default(T), culture, segment); } - public static T Value(this IPublishedProperty property, T defaultValue, int? languageId = null, string segment = null) + public static T Value(this IPublishedProperty property, T defaultValue, string culture = null, string segment = null) { - return property.Value(true, defaultValue, languageId, segment); + return property.Value(true, defaultValue, culture, segment); } - internal static T Value(this IPublishedProperty property, bool withDefaultValue, T defaultValue, int? languageId = null, string segment = null) + internal static T Value(this IPublishedProperty property, bool withDefaultValue, T defaultValue, string culture = null, string segment = null) { - if (property.HasValue(languageId, segment) == false && withDefaultValue) return defaultValue; + if (property.HasValue(culture, segment) == false && withDefaultValue) return defaultValue; // else we use .Value so we give the converter a chance to handle the default value differently // eg for IEnumerable it may return Enumerable.Empty instead of null - var value = property.GetValue(languageId, segment); + var value = property.GetValue(culture, segment); // if value is null (strange but why not) it still is OK to call TryConvertTo // because it's an extension method (hence no NullRef) which will return a diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index c2a818a926..bde74c0ab1 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -48,10 +48,10 @@ namespace Umbraco.Web /// Gets a value indicating whether the content has a value for a property identified by its alias. /// /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is true. - public static bool HasValue(this IPublishedElement content, string alias, int? languageId = null, string segment = null) + public static bool HasValue(this IPublishedElement content, string alias, string culture = null, string segment = null) { var prop = content.GetProperty(alias); - return prop != null && prop.HasValue(languageId, segment); + return prop != null && prop.HasValue(culture, segment); } /// @@ -63,8 +63,7 @@ namespace Umbraco.Web /// The value to return if the content has no value for the property. /// Either or depending on whether the content /// has a value for the property identified by the alias. - public static IHtmlString HasValue(this IPublishedElement content, string alias, - string valueIfTrue, string valueIfFalse = null) + public static IHtmlString IfHasValue(this IPublishedElement content, string alias, string valueIfTrue, string valueIfFalse = null) { return content.HasValue(alias) ? new HtmlString(valueIfTrue) @@ -80,7 +79,7 @@ namespace Umbraco.Web /// /// The content. /// The property alias. - /// The variation language. + /// The variation language. /// The variation segment. /// The value of the content's property identified by the alias. /// @@ -89,10 +88,31 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, int? languageId = null, string segment = null) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null) { var property = content.GetProperty(alias); - return property?.GetValue(languageId, segment); + return property?.GetValue(culture, segment); + } + + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// The default value. + /// The variation language. + /// The variation segment. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object Value(this IPublishedElement content, string alias, string defaultValue, string culture = null, string segment = null) // fixme - kill + { + var property = content.GetProperty(alias); + return property == null || property.HasValue(culture, segment) == false ? defaultValue : property.GetValue(culture, segment); } /// @@ -110,31 +130,10 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string defaultValue, int? languageId = null, string segment = null) // fixme - kill + public static object Value(this IPublishedElement content, string alias, object defaultValue, string culture = null, string segment = null) { var property = content.GetProperty(alias); - return property == null || property.HasValue(languageId, segment) == false ? defaultValue : property.GetValue(languageId, segment); - } - - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The property alias. - /// The default value. - /// The variation language. - /// The variation segment. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object Value(this IPublishedElement content, string alias, object defaultValue, int? languageId = null, string segment = null) - { - var property = content.GetProperty(alias); - return property == null || property.HasValue(languageId, segment) == false ? defaultValue : property.GetValue(languageId, segment); + return property == null || property.HasValue(culture, segment) == false ? defaultValue : property.GetValue(culture, segment); } #endregion @@ -147,7 +146,7 @@ namespace Umbraco.Web /// The target property type. /// The content. /// The property alias. - /// The variation language. + /// The variation language. /// The variation segment. /// The value of the content's property identified by the alias, converted to the specified type. /// @@ -156,9 +155,9 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, int? languageId = null, string segment = null) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null) { - return content.Value(alias, false, default(T), languageId, segment); + return content.Value(alias, false, default(T), culture, segment); } /// @@ -168,7 +167,7 @@ namespace Umbraco.Web /// The content. /// The property alias. /// The default value. - /// The variation language. + /// The variation language. /// The variation segment. /// The value of the content's property identified by the alias, converted to the specified type, if it exists, otherwise a default value. /// @@ -177,17 +176,17 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, T defaultValue, int? languageId = null, string segment = null) + public static T Value(this IPublishedElement content, string alias, T defaultValue, string culture = null, string segment = null) { - return content.Value(alias, true, defaultValue, languageId, segment); + return content.Value(alias, true, defaultValue, culture, segment); } - internal static T Value(this IPublishedElement content, string alias, bool withDefaultValue, T defaultValue, int? languageId = null, string segment = null) // fixme uh? + internal static T Value(this IPublishedElement content, string alias, bool withDefaultValue, T defaultValue, string culture = null, string segment = null) // fixme uh? { var property = content.GetProperty(alias); if (property == null) return defaultValue; - return property.Value(withDefaultValue, defaultValue, languageId, segment); + return property.Value(withDefaultValue, defaultValue, culture, segment); } #endregion diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index 0a399c24c5..525b1fcf2c 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.WebApi.Binders { return ContextMapper.Map>(content, new Dictionary { - [ContextMapper.LanguageKey] = languageId + [ContextMapper.CultureKey] = languageId }); } } diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index ad1ac877be..e940d01470 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -374,17 +374,17 @@ namespace umbraco _content = content; } - public override bool HasValue(int? languageId = null, string segment = null) + public override bool HasValue(string culture = null, string segment = null) { return _sourceValue != null && ((_sourceValue is string) == false || string.IsNullOrWhiteSpace((string)_sourceValue) == false); } - public override object GetSourceValue(int? languageId = null, string segment = null) + public override object GetSourceValue(string culture = null, string segment = null) { return _sourceValue; } - public override object GetValue(int? languageId = null, string segment = null) + public override object GetValue(string culture = null, string segment = null) { // isPreviewing is true here since we want to preview anyway... const bool isPreviewing = true; @@ -392,7 +392,7 @@ namespace umbraco return PropertyType.ConvertInterToObject(_content, PropertyCacheLevel.Unknown, source, isPreviewing); } - public override object GetXPathValue(int? languageId = null, string segment = null) + public override object GetXPathValue(string culture = null, string segment = null) { throw new NotImplementedException(); } From 6b9adfa929e76abb34172d6c954241ebb55077ae Mon Sep 17 00:00:00 2001 From: Stephan Date: Sat, 21 Apr 2018 11:11:31 +0200 Subject: [PATCH 2/4] Move IsCultureAvailable to IContentBase --- src/Umbraco.Core/Models/Content.cs | 4 -- src/Umbraco.Core/Models/ContentBase.cs | 54 ++++++++++++++----------- src/Umbraco.Core/Models/IContent.cs | 9 ----- src/Umbraco.Core/Models/IContentBase.cs | 11 ++++- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 1f106124b4..bb2192f187 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -253,10 +253,6 @@ namespace Umbraco.Core.Models PublishName = null; _publishNames = null; } - - /// - public bool IsCultureAvailable(string culture) - => !string.IsNullOrWhiteSpace(GetName(culture)); /// public bool IsCulturePublished(string culture) diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 32a26f9dc0..ca5f9024a7 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -121,6 +121,22 @@ namespace Umbraco.Core.Models } } + /// + /// Gets the enumeration of property groups for the entity. + /// fixme is a proxy, kill this + /// + [IgnoreDataMember] + public IEnumerable PropertyGroups => ContentTypeBase.CompositionPropertyGroups; + + /// + /// Gets the numeration of property types for the entity. + /// fixme is a proxy, kill this + /// + [IgnoreDataMember] + public IEnumerable PropertyTypes => ContentTypeBase.CompositionPropertyTypes; + + #region Cultures + /// [DataMember] public virtual IReadOnlyDictionary Names @@ -140,7 +156,7 @@ namespace Umbraco.Core.Models { ClearName(culture); return; - } + } if (culture == null) { @@ -158,6 +174,18 @@ namespace Umbraco.Core.Models OnPropertyChanged(Ps.Value.NamesSelector); } + /// + public virtual string GetName(string culture) + { + if (culture == null) return Name; + if (_names == null) return null; + return _names.TryGetValue(culture, out var name) ? name : null; + } + + /// + public bool IsCultureAvailable(string culture) + => !string.IsNullOrWhiteSpace(GetName(culture)); + private void ClearName(string culture) { if (culture == null) @@ -177,28 +205,8 @@ namespace Umbraco.Core.Models _names = null; OnPropertyChanged(Ps.Value.NamesSelector); } - - /// - public virtual string GetName(string culture) - { - if (culture == null) return Name; - if (_names == null) return null; - return _names.TryGetValue(culture, out var name) ? name : null; - } - - /// - /// Gets the enumeration of property groups for the entity. - /// fixme is a proxy, kill this - /// - [IgnoreDataMember] - public IEnumerable PropertyGroups => ContentTypeBase.CompositionPropertyGroups; - - /// - /// Gets the numeration of property types for the entity. - /// fixme is a proxy, kill this - /// - [IgnoreDataMember] - public IEnumerable PropertyTypes => ContentTypeBase.CompositionPropertyTypes; + + #endregion #region Has, Get, Set, Publish Property Value diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 3df81f7314..69e01f4963 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -80,15 +80,6 @@ namespace Umbraco.Core.Models /// ContentStatus Status { 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. - /// - bool IsCultureAvailable(string culture); - /// /// Gets a value indicating whether a given culture is published. /// diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index f8bb0e6985..93a6e82ada 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -51,9 +51,18 @@ namespace Umbraco.Core.Models /// /// Because a dictionary key cannot be null this cannot get nor set the invariant /// name, which must be get or set via the property. - /// + /// IReadOnlyDictionary Names { get; set; } + /// + /// 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. + /// + bool IsCultureAvailable(string culture); + /// /// List of properties, which make up all the data available for this Content object /// From 23333710f5b761b6eb34bcc10b89c98a34a2dcb2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 23 Apr 2018 12:53:17 +0200 Subject: [PATCH 3/4] Implement culture available/edited --- src/Umbraco.Core/EnumExtensions.cs | 22 + .../Install/DatabaseSchemaCreator.cs | 3 +- .../Migrations/Upgrade/UmbracoPlan.cs | 14 +- .../Upgrade/V_8_0_0/AddVariationTables1A.cs | 40 ++ ...riationTable.cs => AddVariationTables2.cs} | 8 +- src/Umbraco.Core/Models/Content.cs | 93 ++-- src/Umbraco.Core/Models/ContentBase.cs | 2 +- src/Umbraco.Core/Models/ContentTypeBase.cs | 2 +- src/Umbraco.Core/Models/IContent.cs | 6 + src/Umbraco.Core/Models/PropertyType.cs | 2 +- .../Persistence/Constants-DatabaseSchema.cs | 7 +- .../Dtos/ContentVersionCultureVariationDto.cs | 16 +- .../Dtos/DocumentCultureVariationDto.cs | 34 ++ .../Persistence/Factories/PropertyFactory.cs | 22 +- .../Implement/DocumentRepository.cs | 183 +++++-- .../Repositories/Implement/MediaRepository.cs | 4 +- .../Implement/MemberRepository.cs | 4 +- src/Umbraco.Core/Umbraco.Core.csproj | 5 +- src/Umbraco.Tests/Models/VariationTests.cs | 63 ++- .../Services/ContentServiceTests.cs | 500 +++++++++++------- .../Services/EntityServiceTests.cs | 8 +- src/Umbraco.Web/Editors/ContentController.cs | 3 +- .../Mapping/ContentItemDisplayNameResolver.cs | 10 +- .../Mapping/ContentTypeProfileExtensions.cs | 12 +- 24 files changed, 752 insertions(+), 311 deletions(-) create mode 100644 src/Umbraco.Core/EnumExtensions.cs create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs rename src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/{AddContentVariationTable.cs => AddVariationTables2.cs} (54%) create mode 100644 src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs new file mode 100644 index 0000000000..58e14bcadf --- /dev/null +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -0,0 +1,22 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Core +{ + /// + /// Provides extension methods for various enumerations. + /// + public static class EnumExtensions + { + /// + /// Determines whether a variation has all flags set. + /// + public static bool Has(this ContentVariation variation, ContentVariation values) + => (variation & values) == values; + + /// + /// Determines whether a variation has at least a flag set. + /// + public static bool HasAny(this ContentVariation variation, ContentVariation values) + => (variation & values) != ContentVariation.Unknown; + } +} diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 6ce300845c..7d4058a0e4 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -82,7 +82,8 @@ namespace Umbraco.Core.Migrations.Install typeof (UserLoginDto), typeof (ConsentDto), typeof (AuditEntryDto), - typeof (ContentVersionCultureVariationDto) + typeof (ContentVersionCultureVariationDto), + typeof (DocumentCultureVariationDto) }; /// diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 8bec74173e..4954742908 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -116,10 +116,11 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); Chain("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); Chain("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); - Chain("{66B6821A-0DE3-4DF8-A6A4-65ABD211EDDE}"); + Chain("{66B6821A-0DE3-4DF8-A6A4-65ABD211EDDE}"); + Chain("{49506BAE-CEBB-4431-A1A6-24AD6EBBBC57}"); // must chain to v8 final state (see at end of file) - Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // UPGRADE FROM 7, MORE RECENT @@ -201,12 +202,17 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{79591E91-01EA-43F7-AC58-7BD286DB1E77}"); // 8.0.0 - Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + // AddVariationTables1 has been superceeded by AddVariationTables2 + //Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); + + // however, need to take care of ppl in post-AddVariationTables1 state + Add("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // FINAL STATE - MUST MATCH LAST ONE ABOVE ! // whenever this changes, update all references in this file! - Add(string.Empty, "{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + Add(string.Empty, "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs new file mode 100644 index 0000000000..df77f34a2c --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs @@ -0,0 +1,40 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class AddVariationTables1A : MigrationBase + { + public AddVariationTables1A(IMigrationContext context) + : base(context) + { } + + // note - original AddVariationTables1 just did + // Create.Table().Do(); + // + // this is taking care of ppl left in this state + + public override void Migrate() + { + // note - original AddVariationTables1 just did + // Create.Table().Do(); + // + // it's been deprecated, not part of the main upgrade path, + // but we need to take care of ppl caught into the state + + // was not used + Delete.Column("available").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + // was not used + Delete.Column("availableDate").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + AddColumn("date"); + + // name, languageId are now non-nullable + AlterColumn(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "name"); + AlterColumn(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "languageId"); + + Create.Table().Do(); + + // fixme - data migration? + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs similarity index 54% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs index efd0f33e99..5b6c913195 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs @@ -1,16 +1,18 @@ -using Umbraco.Core.Persistence.Dtos; +using System; +using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { - public class AddContentVariationTable : MigrationBase + public class AddVariationTables2 : MigrationBase { - public AddContentVariationTable(IMigrationContext context) + public AddVariationTables2(IMigrationContext context) : base(context) { } public override void Migrate() { Create.Table().Do(); + Create.Table().Do(); // fixme - data migration? } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index bb2192f187..22c409d8b1 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -19,8 +19,9 @@ namespace Umbraco.Core.Models private bool _published; private PublishedState _publishedState; private DateTime? _releaseDate; - private DateTime? _expireDate; - private Dictionary _publishNames; + private DateTime? _expireDate; + private Dictionary _publishInfos; + private HashSet _edited; private static readonly Lazy Ps = new Lazy(); @@ -199,37 +200,40 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public string PublishName { get; internal set; } - /// - [IgnoreDataMember] - public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; - - /// - public string GetPublishName(string culture) + // sets publish infos + // internal for repositories + // clear by clearing name + internal void SetPublishInfos(string culture, string name, DateTime date) { - if (culture == null) return PublishName; - if (_publishNames == null) return null; - return _publishNames.TryGetValue(culture, out var name) ? name : null; - } - - // sets a publish name - // internal for repositories - internal void SetPublishName(string culture, string name) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullOrEmptyException(nameof(name)); if (culture == null) { PublishName = name; + PublishDate = date; return; } // private method, assume that culture is valid - if (_publishNames == null) - _publishNames = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (_publishInfos == null) + _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - _publishNames[culture] = name; + _publishInfos[culture] = (name, date); + } + + /// + [IgnoreDataMember] + //public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name) ?? NoNames; + + /// + public string GetPublishName(string culture) + { + if (culture == null) return PublishName; + if (_publishInfos == null) return null; + return _publishInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } // clears a publish name @@ -241,23 +245,51 @@ namespace Umbraco.Core.Models return; } - if (_publishNames == null) return; - _publishNames.Remove(culture); - if (_publishNames.Count == 0) - _publishNames = null; + if (_publishInfos == null) return; + _publishInfos.Remove(culture); + if (_publishInfos.Count == 0) + _publishInfos = null; } // clears all publish names private void ClearPublishNames() { PublishName = null; - _publishNames = null; + _publishInfos = null; } /// public bool IsCulturePublished(string culture) => !string.IsNullOrWhiteSpace(GetPublishName(culture)); + /// + public DateTime GetDateCulturePublished(string culture) + { + if (_publishInfos != null && _publishInfos.TryGetValue(culture, out var infos)) + return infos.Date; + throw new InvalidOperationException($"Culture \"{culture}\" is not published."); + } + + /// + public bool IsCultureEdited(string culture) + { + return string.IsNullOrWhiteSpace(GetPublishName(culture)) || (_edited != null && _edited.Contains(culture)); + } + + // sets a publish edited + internal void SetCultureEdited(string culture) + { + if (_edited == null) + _edited = new HashSet(StringComparer.OrdinalIgnoreCase); + _edited.Add(culture); + } + + // sets all publish edited + internal void SetCultureEdited(IEnumerable cultures) + { + _edited = new HashSet(cultures, StringComparer.OrdinalIgnoreCase); + } + [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -276,11 +308,12 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(Name)) throw new InvalidOperationException($"Cannot publish invariant culture without a name."); PublishName = Name; + var now = DateTime.Now; foreach (var (culture, name) in Names) { if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishName(culture, name); + SetPublishInfos(culture, name, now); } // property.PublishAllValues only deals with supported variations (if any) @@ -308,7 +341,7 @@ namespace Umbraco.Core.Models var name = GetName(culture); if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishName(culture, name); + SetPublishInfos(culture, name, DateTime.Now); } // property.PublishValue throws on invalid variation, so filter them out @@ -331,7 +364,7 @@ namespace Umbraco.Core.Models var name = GetName(culture); if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishName(culture, name); + SetPublishInfos(culture, name, DateTime.Now); // property.PublishCultureValues only deals with supported variations (if any) foreach (var property in Properties) diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index ca5f9024a7..7bc327c2d0 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -164,7 +164,7 @@ namespace Umbraco.Core.Models return; } - if ((ContentTypeBase.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) == 0) + if (!ContentTypeBase.Variations.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) throw new NotSupportedException("Content type does not support varying name by culture."); if (_names == null) diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 7d0ffea31e..952fa0cd88 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -221,7 +221,7 @@ namespace Umbraco.Core.Models { variation = ContentVariation.InvariantNeutral; } - if ((Variations & variation) == 0) + if (!Variations.Has(variation)) { if (throwIfInvalid) throw new NotSupportedException($"Variation {variation} is invalid for content type \"{Alias}\"."); diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 69e01f4963..ab995b9b04 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -89,6 +89,12 @@ namespace Umbraco.Core.Models /// whenever values for this culture are unpublished. /// bool IsCulturePublished(string culture); + + // fixme doc + DateTime GetDateCulturePublished(string culture); + + // fixme doc + bool IsCultureEdited(string culture); /// /// Gets the name of the published version of the content for a given culture. diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index b34eca7c57..66299e7749 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -243,7 +243,7 @@ namespace Umbraco.Core.Models { variation = ContentVariation.InvariantNeutral; } - if ((Variations & variation) == 0) + if (!Variations.Has(variation)) { if (throwIfInvalid) throw new NotSupportedException($"Variation {variation} is invalid for property type \"{Alias}\"."); diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 68afd4f244..79dd45b73b 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -17,10 +17,10 @@ namespace Umbraco.Core public const string NodeXml = /*TableNamePrefix*/ "cms" + "ContentXml"; public const string NodePreviewXml = /*TableNamePrefix*/ "cms" + "PreviewXml"; // fixme dbfix kill merge with ContentXml - public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; // fixme dbfixrename and split uElementType, uDocumentType + public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; // fixme dbfix rename and split uElementType, uDocumentType public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType"; - public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; // fixme dbfixmust rename corresponding DTO - public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; // fixme dbfixwhy can't we just use uNode for this? + public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; // fixme dbfix must rename corresponding DTO + public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; // fixme dbfix why can't we just use uNode for this? public const string DataType = TableNamePrefix + "DataType"; public const string Template = /*TableNamePrefix*/ "cms" + "Template"; @@ -28,6 +28,7 @@ namespace Umbraco.Core public const string ContentVersion = TableNamePrefix + "ContentVersion"; public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; public const string Document = TableNamePrefix + "Document"; + public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; public const string MediaVersion = TableNamePrefix + "MediaVersion"; diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs index 104a707669..dbba897f7a 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -25,21 +25,21 @@ namespace Umbraco.Core.Persistence.Dtos [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] public int LanguageId { get; set; } + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string Culture { get; set; } + [Column("name")] - [NullSetting(NullSetting = NullSettings.Null)] public string Name { get; set; } - [Column("available")] - public bool Available { get; set; } - - [Column("availableDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? AvailableDate { get; set; } + [Column("date")] + public DateTime Date { get; set; } + // fixme want? [Column("availableUserId")] // [ForeignKey(typeof(UserDto))] -- there is no foreign key so we can delete users without deleting associated content //[NullSetting(NullSetting = NullSettings.Null)] - public int AvailableUserId { get; set; } + public int PublishedUserId { get; set; } [Column("edited")] public bool Edited { get; set; } diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs new file mode 100644 index 0000000000..be4cf7c023 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs @@ -0,0 +1,34 @@ +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Persistence.Dtos +{ + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class DocumentCultureVariationDto + { + private const string TableName = Constants.DatabaseSchema.Tables.DocumentCultureVariation; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] + public int NodeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } + + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string Culture { get; set; } + + [Column("edited")] + public bool Edited { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 10f2918204..d25f921f1a 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -4,7 +4,6 @@ using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Factories { @@ -90,15 +89,25 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - public static IEnumerable BuildDtos(int currentVersionId, int publishedVersionId, IEnumerable properties, ILanguageRepository languageRepository, out bool edited) + public static IEnumerable BuildDtos(int currentVersionId, int publishedVersionId, IEnumerable properties, ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures) { var propertyDataDtos = new List(); edited = false; + editedCultures = null; // don't allocate unless necessary foreach (var property in properties) { if (property.PropertyType.IsPublishing) - { + { + // fixme + // why only CultureNeutral? + // then, the tree can only show when a CultureNeutral value has been modified, but not when + // a CultureSegment has been modified, so if I edit some french/mobile thing, the tree will + // NOT tell me that I have changes? + + var editingCultures = property.PropertyType.Variations.Has(ContentVariation.CultureNeutral); + if (editingCultures && editedCultures == null) editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { @@ -117,6 +126,13 @@ namespace Umbraco.Core.Persistence.Factories // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); edited |= !sameValues; + + if (editingCultures && // cultures can be edited, ie CultureNeutral is supported + propertyValue.Culture != null && propertyValue.Segment == null && // and value is CultureNeutral + !sameValues) // and edited and published are different + { + editedCultures.Add(propertyValue.Culture); // report culture as edited + } } } else diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 848019cf95..e47e8aa33e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -315,10 +315,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited); + var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); + // name also impacts 'edited' + if (content.PublishName != content.Name) + edited = true; + // persist the document dto // at that point, when publishing, the entity still has its old Published value // so we need to explicitely update the dto to persist the correct value @@ -329,12 +333,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the variations - if ((content.ContentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) + if (content.ContentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)) { - foreach (var variationDto in GetVariationDtos(content, publishing)) - Database.Insert(variationDto); + // names also impact 'edited' + foreach (var (culture, name) in content.Names) + if (name != content.GetPublishName(culture)) + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); + + // insert content variations + Database.InsertBulk(GetContentVariationDtos(content, publishing)); + + // insert document variations + Database.InsertBulk(GetDocumentVariationDtos(content, publishing, editedCultures)); } + // refresh content + if (editedCultures != null) + content.SetCultureEdited(editedCultures); + // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -454,26 +470,51 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(documentVersionDto); } - var versionToDelete = publishing ? new[] { content.VersionId, content.PublishedVersionId } : new[] { content.VersionId }; - - // replace the property data - var deletePropertyDataSql = Sql().Delete().WhereIn(x => x.VersionId, versionToDelete); + // replace the property data (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var versionToDelete = publishing ? content.PublishedVersionId : content.VersionId; + var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, LanguageRepository, out var edited); + // insert property data + var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // replace the variations - if ((content.ContentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) - { - var deleteVariations = Sql().Delete().WhereIn(x => x.VersionId, versionToDelete); - Database.Execute(deleteVariations); + // name also impacts 'edited' + if (content.PublishName != content.Name) + edited = true; - foreach (var variationDto in GetVariationDtos(content, publishing)) - Database.Insert(variationDto); + if (content.ContentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)) + { + // names also impact 'edited' + foreach (var (culture, name) in content.Names) + if (name != content.GetPublishName(culture)) + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); + + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); + + // replace the document version variations (rather than updating) + var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); + Database.Execute(deleteDocumentVariations); + + // fixme is it OK to use NPoco InsertBulk here, or should we use our own BulkInsertRecords? + // (same in PersistNewItem above) + + // insert content variations + Database.InsertBulk(GetContentVariationDtos(content, publishing)); + + // insert document variations + Database.InsertBulk(GetDocumentVariationDtos(content, publishing, editedCultures)); } + // refresh content + if (editedCultures != null) + content.SetCultureEdited(editedCultures); + // update the document dto // at that point, when un/publishing, the entity still has its old Published value // so we need to explicitely update the dto to persist the correct value @@ -853,12 +894,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // set variations, if varying - temps = temps.Where(x => x.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)).ToList(); + temps = temps.Where(x => x.ContentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)).ToList(); if (temps.Count > 0) { - var variations = GetVariations(temps); + // load all variations for all documents from database, in one query + var contentVariations = GetContentVariations(temps); + var documentVariations = GetDocumentVariations(temps); foreach (var temp in temps) - SetContentVariations(temp.Content, variations); + SetVariations(temp.Content, contentVariations, documentVariations); } return content; @@ -886,10 +929,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Properties = properties[dto.DocumentVersionDto.Id]; // set variations, if varying - if ((contentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) + if (contentType.Variations.HasAny(Models.ContentVariation.CultureNeutral | Models.ContentVariation.CultureSegment)) { - var variations = GetVariations(ltemp); - SetContentVariations(content, variations); + var contentVariations = GetContentVariations(ltemp); + var documentVariations = GetDocumentVariations(ltemp); + SetVariations(content, contentVariations, documentVariations); } // reset dirty initial properties (U4-1946) @@ -897,21 +941,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return content; } - private void SetContentVariations(Content content, IDictionary> variations) + private void SetVariations(Content content, IDictionary> contentVariations, IDictionary> documentVariations) { - if (variations.TryGetValue(content.VersionId, out var variation)) - foreach (var v in variation) - { + if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) + foreach (var v in contentVariation) content.SetName(v.Culture, v.Name); - } - if (content.PublishedVersionId > 0 && variations.TryGetValue(content.PublishedVersionId, out variation)) - foreach (var v in variation) - { - content.SetPublishName(v.Culture, v.Name); - } + if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) + foreach (var v in contentVariation) + content.SetPublishInfos(v.Culture, v.Name, v.Date); + if (documentVariations.TryGetValue(content.Id, out var documentVariation)) + foreach (var v in documentVariation.Where(x => x.Edited)) + content.SetCultureEdited(v.Culture); } - private IDictionary> GetVariations(List> temps) + private IDictionary> GetContentVariations(List> temps) where T : class, IContentBase { var versions = new List(); @@ -921,7 +964,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (temp.PublishedVersionId > 0) versions.Add(temp.PublishedVersionId); } - if (versions.Count == 0) return new Dictionary>(); + if (versions.Count == 0) return new Dictionary>(); var dtos = Database.FetchByGroups(versions, 2000, batch => Sql() @@ -929,50 +972,104 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .From() .WhereIn(x => x.VersionId, batch)); - var variations = new Dictionary>(); + var variations = new Dictionary>(); foreach (var dto in dtos) { if (!variations.TryGetValue(dto.VersionId, out var variation)) - variations[dto.VersionId] = variation = new List(); + variations[dto.VersionId] = variation = new List(); - variation.Add(new CultureVariation + variation.Add(new ContentVariation { Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Name = dto.Name, - Available = dto.Available + Date = dto.Date }); } return variations; } - private IEnumerable GetVariationDtos(IContent content, bool publishing) + private IDictionary> GetDocumentVariations(List> temps) + where T : class, IContentBase { + var ids = temps.Select(x => x.Id); + + var dtos = Database.FetchByGroups(ids, 2000, batch => + Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch)); + + var variations = new Dictionary>(); + + foreach (var dto in dtos) + { + if (!variations.TryGetValue(dto.NodeId, out var variation)) + variations[dto.NodeId] = variation = new List(); + + variation.Add(new DocumentVariation + { + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), + Edited = dto.Edited + }); + } + + return variations; + } + + private IEnumerable GetContentVariationDtos(IContent content, bool publishing) + { + // create dtos for the 'current' (non-published) version, all cultures foreach (var (culture, name) in content.Names) yield return new ContentVersionCultureVariationDto { VersionId = content.VersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Name = name + Culture = culture, + Name = name, + Date = content.UpdateDate }; + // if not publishing, we're just updating the 'current' (non-published) version, + // so there are no DTOs to create for the 'published' version which remains unchanged if (!publishing) yield break; + // create dtos for the 'published' version, for published cultures (those having a name) foreach (var (culture, name) in content.PublishNames) yield return new ContentVersionCultureVariationDto { VersionId = content.PublishedVersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Name = name + Culture = culture, + Name = name, + Date = content.GetDateCulturePublished(culture) }; } - private class CultureVariation + private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures) + { + foreach (var (culture, name) in content.Names) + yield return new DocumentCultureVariationDto + { + NodeId = content.Id, + LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), + Culture = culture, + Edited = !content.IsCulturePublished(culture) || editedCultures.Contains(culture) // if not published, always edited + }; + } + + private class ContentVariation { public string Culture { get; set; } public string Name { get; set; } - public bool Available { get; set; } + public DateTime Date { get; set; } + } + + private class DocumentVariation + { + public string Culture { get; set; } + public bool Edited { get; set; } } #region Utilities diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index dfb30a9cb3..982a5bb885 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -279,7 +279,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -336,7 +336,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 04e5d64b06..98c38603b1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -306,7 +306,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -371,7 +371,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == member.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7549d4ba2b..12631dc8fe 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -320,6 +320,7 @@ + @@ -336,7 +337,8 @@ - + + @@ -369,6 +371,7 @@ + diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 70e3d574b3..5bfd3f2de5 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -164,22 +164,26 @@ namespace Umbraco.Tests.Models const string langFr = "fr-FR"; const string langUk = "en-UK"; + // throws if the content type does not support the variation Assert.Throws(() => content.SetName(langFr, "name-fr")); + // now it will work contentType.Variations = ContentVariation.CultureNeutral; + // invariant name works content.Name = "name"; Assert.AreEqual("name", content.GetName(null)); content.SetName(null, "name2"); Assert.AreEqual("name2", content.Name); Assert.AreEqual("name2", content.GetName(null)); + // variant names work content.SetName(langFr, "name-fr"); content.SetName(langUk, "name-uk"); - Assert.AreEqual("name-fr", content.GetName(langFr)); Assert.AreEqual("name-uk", content.GetName(langUk)); + // variant dictionary of names work Assert.AreEqual(2, content.Names.Count); Assert.IsTrue(content.Names.ContainsKey(langFr)); Assert.AreEqual("name-fr", content.Names[langFr]); @@ -188,7 +192,7 @@ namespace Umbraco.Tests.Models } [Test] - public void ContentTests() + public void ContentPublishValues() { const string langFr = "fr-FR"; @@ -296,6 +300,61 @@ namespace Umbraco.Tests.Models Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); } + [Test] + public void ContentPublishVariations() + { + const string langFr = "fr-FR"; + const string langUk = "en-UK"; + const string langEs = "es-ES"; + + var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop" }; + var contentType = new ContentType(-1) { Alias = "contentType" }; + contentType.AddPropertyType(propertyType); + + var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; + + contentType.Variations |= ContentVariation.CultureNeutral; + propertyType.Variations |= ContentVariation.CultureNeutral; + + content.SetValue("prop", "a"); + content.SetValue("prop", "a-fr", langFr); + content.SetValue("prop", "a-uk", langUk); + content.SetValue("prop", "a-es", langEs); + + // cannot publish without a name + Assert.Throws(() => content.PublishValues(langFr)); + + // works with a name + // and then FR is available, and published + content.SetName(langFr, "name-fr"); + content.PublishValues(langFr); + + // now UK is available too + content.SetName(langUk, "name-uk"); + + // test available, published + Assert.IsTrue(content.IsCultureAvailable(langFr)); + Assert.IsTrue(content.IsCulturePublished(langFr)); + Assert.AreEqual("name-fr", content.GetPublishName(langFr)); + Assert.AreNotEqual(DateTime.MinValue, content.GetDateCulturePublished(langFr)); + Assert.IsFalse(content.IsCultureEdited(langFr)); // once published, edited is *wrong* until saved + + Assert.IsTrue(content.IsCultureAvailable(langUk)); + Assert.IsFalse(content.IsCulturePublished(langUk)); + Assert.IsNull(content.GetPublishName(langUk)); + Assert.Throws(() => content.GetDateCulturePublished(langUk)); // not published! + Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited + + Assert.IsFalse(content.IsCultureAvailable(langEs)); + Assert.IsFalse(content.IsCulturePublished(langEs)); + Assert.IsNull(content.GetPublishName(langEs)); + Assert.Throws(() => content.GetDateCulturePublished(langEs)); // not published! + Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited + + // cannot test IsCultureEdited here - as that requires the content service and repository + // see: ContentServiceTests.Can_SaveRead_Variations + } + [Test] public void IsDirtyTests() { diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index b3bdf4f1ba..c45d9be5f3 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2475,231 +2475,351 @@ namespace Umbraco.Tests.Services } [Test] - public void Can_SaveAndRead_Names() + public void Can_SaveRead_Variations() { var languageService = ServiceContext.LocalizationService; var langFr = new Language("fr-FR"); var langUk = new Language("en-UK"); - var langDe = new Language("de-DE"); + var langDe = new Language("de-DE"); + languageService.Save(langFr); languageService.Save(langUk); - languageService.Save(langDe); + languageService.Save(langDe); var contentTypeService = ServiceContext.ContentTypeService; - - // fixme - // contentType.Variations is InvariantNeutral | CultureNeutral - // propertyType.Variations can only be a subset of contentType.Variations - ie cannot *add* anything - // (at least, we should validate this) - // but then, - // if the contentType supports InvariantNeutral | CultureNeutral, - // the propertyType should support InvariantNeutral, or both, but not solely CultureNeutral? - // but does this mean that CultureNeutral implies InvariantNeutral? - // can a contentType *not* support InvariantNeutral? - - var contentType = contentTypeService.Get("umbTextpage"); + + // fixme + // contentType.Variations is InvariantNeutral | CultureNeutral + // propertyType.Variations can only be a subset of contentType.Variations - ie cannot *add* anything + // (at least, we should validate this) + // but then, + // if the contentType supports InvariantNeutral | CultureNeutral, + // the propertyType should support InvariantNeutral, or both, but not solely CultureNeutral? + // but does this mean that CultureNeutral implies InvariantNeutral? + // can a contentType *not* support InvariantNeutral? + + var contentType = contentTypeService.Get("umbTextpage"); contentType.Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral; contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.CultureNeutral }); - contentTypeService.Save(contentType); - + contentTypeService.Save(contentType); + var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage"); + var content = contentService.Create("Home US", - 1, "umbTextpage"); + + // act + + content.SetValue("author", "Barack Obama"); + content.SetValue("prop", "value-fr1", langFr.IsoCode); + content.SetValue("prop", "value-uk1", langUk.IsoCode); + content.SetName(langFr.IsoCode, "name-fr"); + content.SetName(langUk.IsoCode, "name-uk"); + contentService.Save(content); + + // content has been saved, + // it has names, but no publishNames, and no published cultures + + var content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.IsNull(content2.GetValue("prop", langUk.IsoCode, published: true)); + + Assert.IsNull(content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.IsNull(content2.GetPublishName(langUk.IsoCode)); + + // only fr and uk have a name, and are available + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // nothing has been published yet + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, false), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, false), (langDe, false)); + + // not published => must be edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + // act + + content.PublishValues(langFr.IsoCode); + content.PublishValues(langUk.IsoCode); + contentService.SaveAndPublish(content); + + // both FR and UK have been published, + // and content has been published, + // it has names, publishNames, and published cultures + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + + Assert.IsNull(content2.PublishName); // we haven't published InvariantNeutral + Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr and uk have been published now + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr and uk, published without changes, not edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); - // act - - content.SetValue("author", "Barack Obama"); - content.SetValue("prop", "value-fr1", langFr.IsoCode); - content.SetValue("prop", "value-uk1", langUk.IsoCode); - content.SetName(langFr.IsoCode, "name-fr"); - content.SetName(langUk.IsoCode, "name-uk"); - contentService.Save(content); + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - // content has been saved, - // it has names, but no publishNames, and no published cultures + // note that content and content2 culture published dates might be slightly different due to roundtrip to database - var content2 = contentService.GetById(content.Id); + + // act + + content.PublishValues(); + contentService.SaveAndPublish(content); + + // now it has publish name for invariant neutral + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.PublishName); - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + + // act + + content.SetName(null, "Home US2"); + content.SetName(langFr.IsoCode, "name-fr2"); + content.SetName(langUk.IsoCode, "name-uk2"); + content.SetValue("author", "Barack Obama2"); + content.SetValue("prop", "value-fr2", langFr.IsoCode); + content.SetValue("prop", "value-uk2", langUk.IsoCode); + contentService.Save(content); + + // content has been saved, + // it has updated names, unchanged publishNames, and published cultures + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("Barack Obama2", content2.GetValue("author")); + Assert.AreEqual("Barack Obama", content2.GetValue("author", published: true)); + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, true), (langUk, true), (langDe, false)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.IsNull(content2.GetValue("prop", langUk.IsoCode, published: true)); + // we have changed values so now fr and uk are edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - Assert.IsNull(content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.IsNull(content2.GetPublishName(langUk.IsoCode)); + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - Assert.IsTrue(content.IsCultureAvailable(langFr.IsoCode)); - Assert.IsTrue(content.IsCultureAvailable(langUk.IsoCode)); - Assert.IsFalse(content.IsCultureAvailable(langDe.IsoCode)); + + // act + // cannot just 'save' since we are changing what's published! + + content.ClearPublishedValues(langFr.IsoCode); + contentService.SaveAndPublish(content); + + // content has been published, + // the french culture is gone + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr is not published anymore + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // and so, fr has to be edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); - // act + // act - content.PublishValues(langFr.IsoCode); - content.PublishValues(langUk.IsoCode); - contentService.SaveAndPublish(content); + contentService.Unpublish(content); + + // content has been unpublished, + // but properties, names, etc. retain their 'published' values so the content + // can be re-published in its exact original state (before being unpublished) + // + // BEWARE! + // in order for a content to be unpublished as a whole, and then republished in + // its exact previous state, properties and names etc. retain their published + // values even though the content is not published - hence many things being + // non-null or true below - always check against content.Published to be sure + + content2 = contentService.GetById(content.Id); + + Assert.IsFalse(content2.Published); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); // not null, see note above + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); // not null, see note above + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); // has value, see note above + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // fr is not published anymore - uk still is, see note above + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // and so, fr has to be edited - uk still is + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - // both FR and UK have been published, - // and content has been published, - // it has names, publishNames, and published cultures - content2 = contentService.GetById(content.Id); + // act - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + contentService.SaveAndPublish(content); - Assert.IsNull(content2.PublishName); // we haven't published InvariantNeutral - Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + // content has been re-published, + // everything is back to what it was before being unpublished - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + content2 = contentService.GetById(content.Id); + + Assert.IsTrue(content2.Published); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); + + Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); + Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); + Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); + Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // no change, back to published + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // no change, back to published + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - // act + // act - content.PublishValues(); - contentService.SaveAndPublish(content); + content.PublishValues(langUk.IsoCode); + contentService.SaveAndPublish(content); + + content2 = contentService.GetById(content.Id); + + // no change + AssertPerCulture(content, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCultureAvailable(c), (langFr, true), (langUk, true), (langDe, false)); + + // no change + AssertPerCulture(content, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + AssertPerCulture(content2, (x, c) => x.IsCulturePublished(c), (langFr, false), (langUk, true), (langDe, false)); + + // now, uk is no more edited + AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); + AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); + + AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - // now it has publish name for invariant neutral - - content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.PublishName); + // act - // act + content.SetName(langUk.IsoCode, "name-uk3"); + contentService.Save(content); + + content2 = contentService.GetById(content.Id); - content.SetName(null, "Home US2"); - content.SetName(langFr.IsoCode, "name-fr2"); - content.SetName(langUk.IsoCode, "name-uk2"); - content.SetValue("author", "Barack Obama2"); - content.SetValue("prop", "value-fr2", langFr.IsoCode); - content.SetValue("prop", "value-uk2", langUk.IsoCode); - contentService.Save(content); - - // content has been saved, - // it has updated names, unchanged publishNames, and published cultures + // changing the name = edited! + Assert.IsTrue(content.IsCultureEdited(langUk.IsoCode)); + Assert.IsTrue(content2.IsCultureEdited(langUk.IsoCode)); + } - content2 = contentService.GetById(content.Id); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); - Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - - Assert.AreEqual("Barack Obama2", content2.GetValue("author")); - Assert.AreEqual("Barack Obama", content2.GetValue("author", published: true)); - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - - Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - - // act - // cannot just 'save' since we are changing what's published! - - content.ClearPublishedValues(langFr.IsoCode); - contentService.SaveAndPublish(content); - - // content has been published, - // the french culture is gone - - content2 = contentService.GetById(content.Id); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - - // act - - contentService.Unpublish(content); - - // content has been unpublished, - // but properties, names, etc. retain their 'published' values so the content - // can be re-published in its exact original state (before being unpublished) - // - // BEWARE! - // in order for a content to be unpublished as a whole, and then republished in - // its exact previous state, properties and names etc. retain their published - // values even though the content is not published - hence many things being - // non-null or true below - always check against content.Published to be sure - - content2 = contentService.GetById(content.Id); - - Assert.IsFalse(content2.Published); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); // not null, see note above - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); // not null, see note above - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); // has value, see note above - - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); // still true, see note above - - // act - - contentService.SaveAndPublish(content); - - // content has been re-published, - // everything is back to what it was before being unpublished - - content2 = contentService.GetById(content.Id); - - Assert.IsTrue(content2.Published); - - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); - - Assert.AreEqual("Home US", content2.PublishName); - Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); - - Assert.AreEqual("value-fr2", content2.GetValue("prop", langFr.IsoCode)); - Assert.AreEqual("value-uk2", content2.GetValue("prop", langUk.IsoCode)); - Assert.IsNull(content2.GetValue("prop", langFr.IsoCode, published: true)); - Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode, published: true)); - - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + private void AssertPerCulture(IContent item, Func getter, params (ILanguage Language, bool Result)[] testCases) + { + foreach (var testCase in testCases) + { + var value = getter(item, testCase.Language.IsoCode); + Assert.AreEqual(testCase.Result, value, $"Expected {testCase.Result} and got {value} for culture {testCase.Language.IsoCode}."); + } } private IEnumerable CreateContentHierarchy() diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 124c77846d..8d8e127131 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -457,8 +457,8 @@ namespace Umbraco.Tests.Services ServiceContext.ContentTypeService.Save(contentType); var c1 = MockedContent.CreateSimpleContent(contentType, "Test", -1); - c1.SetName(_langFr.Id, "Test - FR"); - c1.SetName(_langEs.Id, "Test - ES"); + c1.SetName(_langFr.IsoCode, "Test - FR"); + c1.SetName(_langEs.IsoCode, "Test - ES"); ServiceContext.ContentService.Save(c1); var result = service.Get(c1.Id, UmbracoObjectTypes.Document); @@ -486,8 +486,8 @@ namespace Umbraco.Tests.Services var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); if (i % 2 == 0) { - c1.SetName(_langFr.Id, "Test " + i + " - FR"); - c1.SetName(_langEs.Id, "Test " + i + " - ES"); + c1.SetName(_langFr.IsoCode, "Test " + i + " - FR"); + c1.SetName(_langEs.IsoCode, "Test " + i + " - ES"); } ServiceContext.ContentService.Save(c1); } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 467562428d..6ce5f049e8 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -956,7 +956,8 @@ namespace Umbraco.Web.Editors //set the name according to the culture settings if (contentItem.LanguageId.HasValue && contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) { - contentItem.PersistedContent.SetName(contentItem.LanguageId, contentItem.Name); + var culture = Services.LocalizationService.GetLanguageById(contentItem.LanguageId.Value).IsoCode; + contentItem.PersistedContent.SetName(culture, contentItem.Name); } else { diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs index 125db912be..41383764bb 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs @@ -12,14 +12,8 @@ namespace Umbraco.Web.Models.Mapping { public string Resolve(IContent source, ContentItemDisplay destination, string destMember, ResolutionContext context) { - var langId = context.GetLanguageId(); - if (langId.HasValue && source.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) - { - //return the culture name being requested - return source.GetName(langId); - } - - return source.Name; + var culture = context.GetCulture(); + return source.GetName(culture); } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs index 72842c5354..1348dbb8a9 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeProfileExtensions.cs @@ -163,7 +163,7 @@ namespace Umbraco.Web.Models.Mapping // fixme not so clean really var isPublishing = typeof(IContentType).IsAssignableFrom(typeof(TDestination)); - return mapping + mapping = mapping //only map id if set to something higher then zero .ForMember(dest => dest.Id, opt => opt.Condition(src => (Convert.ToInt32(src.Id) > 0))) .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Convert.ToInt32(src.Id))) @@ -179,10 +179,14 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.PropertyGroups, opt => opt.Ignore()) .ForMember(dest => dest.NoGroupPropertyTypes, opt => opt.Ignore()) // ignore, composition is managed in AfterMapContentTypeSaveToEntity - .ForMember(dest => dest.ContentTypeComposition, opt => opt.Ignore()) + .ForMember(dest => dest.ContentTypeComposition, opt => opt.Ignore()); - .ForMember(dto => dto.Variations, opt => opt.ResolveUsing>()) + // ignore for members + mapping = typeof(TDestination) == typeof(IMemberType) + ? mapping.ForMember(dto => dto.Variations, opt => opt.Ignore()) + : mapping.ForMember(dto => dto.Variations, opt => opt.ResolveUsing>()); + mapping = mapping .ForMember( dest => dest.AllowedContentTypes, opt => opt.MapFrom(src => src.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)))) @@ -257,6 +261,8 @@ namespace Umbraco.Web.Models.Mapping // because all property collections were rebuilt, there is no need to remove // some old properties, they are just gone and will be cleared by the repository }); + + return mapping; } private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, IEnumerable destOrigGroups) From 7b82208677a033b0ee1c2bdcd40ffe0120e5098c Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 24 Apr 2018 17:43:08 +0200 Subject: [PATCH 4/4] Implement getting avail/edit/publish cultures --- src/Umbraco.Core/Models/Content.cs | 85 ++++++++++++++++------------- src/Umbraco.Core/Models/IContent.cs | 27 ++++++++- 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 22c409d8b1..4ead233d6a 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models private bool _published; private PublishedState _publishedState; private DateTime? _releaseDate; - private DateTime? _expireDate; + private DateTime? _expireDate; private Dictionary _publishInfos; private HashSet _edited; @@ -196,12 +196,12 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public ITemplate PublishTemplate { get; internal set; } - + [IgnoreDataMember] public string PublishName { get; internal set; } // sets publish infos - // internal for repositories + // internal for repositories // clear by clearing name internal void SetPublishInfos(string culture, string name, DateTime date) { @@ -222,12 +222,12 @@ namespace Umbraco.Core.Models _publishInfos[culture] = (name, date); } - + /// [IgnoreDataMember] //public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name) ?? NoNames; - + /// public string GetPublishName(string culture) { @@ -235,7 +235,7 @@ namespace Umbraco.Core.Models if (_publishInfos == null) return null; return _publishInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } - + // clears a publish name private void ClearPublishName(string culture) { @@ -250,18 +250,18 @@ namespace Umbraco.Core.Models if (_publishInfos.Count == 0) _publishInfos = null; } - + // clears all publish names private void ClearPublishNames() { PublishName = null; _publishInfos = null; } - + /// public bool IsCulturePublished(string culture) => !string.IsNullOrWhiteSpace(GetPublishName(culture)); - + /// public DateTime GetDateCulturePublished(string culture) { @@ -270,26 +270,35 @@ namespace Umbraco.Core.Models throw new InvalidOperationException($"Culture \"{culture}\" is not published."); } + /// + public IEnumerable PublishedCultures => _publishInfos.Keys; + /// public bool IsCultureEdited(string culture) { return string.IsNullOrWhiteSpace(GetPublishName(culture)) || (_edited != null && _edited.Contains(culture)); - } - + } + // sets a publish edited internal void SetCultureEdited(string culture) { if (_edited == null) _edited = new HashSet(StringComparer.OrdinalIgnoreCase); _edited.Add(culture); - } - + } + // sets all publish edited internal void SetCultureEdited(IEnumerable cultures) { _edited = new HashSet(cultures, StringComparer.OrdinalIgnoreCase); } - + + /// + public IEnumerable EditedCultures => Names.Keys.Where(IsCultureEdited); + + /// + public IEnumerable AvailableCultures => Names.Keys; + [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -303,7 +312,7 @@ namespace Umbraco.Core.Models if (ValidateAll().Any()) return false; - // Name and PublishName are managed by the repository, but Names and PublishNames + // 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."); @@ -319,21 +328,21 @@ namespace Umbraco.Core.Models // property.PublishAllValues only deals with supported variations (if any) foreach (var property in Properties) property.PublishAllValues(); - + _publishedState = PublishedState.Publishing; return true; } /// public virtual bool PublishValues(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 + ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); + // the values we want to publish should be valid if (Validate(culture, segment).Any()) return false; - + // 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) @@ -347,18 +356,18 @@ namespace Umbraco.Core.Models // 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); - + _publishedState = PublishedState.Publishing; return true; } /// public virtual bool PublishCultureValues(string culture = null) - { + { // the values we want to publish should be valid if (ValidateCulture(culture).Any()) return false; - + // 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); @@ -369,7 +378,7 @@ namespace Umbraco.Core.Models // property.PublishCultureValues only deals with supported variations (if any) foreach (var property in Properties) property.PublishCultureValues(culture); - + _publishedState = PublishedState.Publishing; return true; } @@ -383,8 +392,8 @@ namespace Umbraco.Core.Models // 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(); - + ClearPublishNames(); + _publishedState = PublishedState.Publishing; } @@ -393,7 +402,7 @@ namespace Umbraco.Core.Models { // 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); @@ -415,10 +424,10 @@ namespace Umbraco.Core.Models // 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; } - + private bool CopyingFromSelf(IContent other) { // copying from the same Id and VersionPk @@ -456,10 +465,10 @@ namespace Umbraco.Core.Models var value = published ? pvalue.PublishedValue : pvalue.EditedValue; SetValue(alias, value, pvalue.Culture, pvalue.Segment); } - } - + } + // copy names - ClearNames(); + ClearNames(); foreach (var (languageId, name) in other.Names) SetName(languageId, name); Name = other.Name; @@ -498,9 +507,9 @@ namespace Umbraco.Core.Models var alias = otherProperty.PropertyType.Alias; SetValue(alias, otherProperty.GetValue(culture, segment, published), culture, segment); - } + } - // copy name + // copy name SetName(culture, other.GetName(culture)); } @@ -532,9 +541,9 @@ namespace Umbraco.Core.Models var value = published ? pvalue.PublishedValue : pvalue.EditedValue; SetValue(alias, value, pvalue.Culture, pvalue.Segment); } - } - - // copy name + } + + // copy name SetName(culture, other.GetName(culture)); } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index ab995b9b04..25f980a597 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -90,10 +90,18 @@ namespace Umbraco.Core.Models /// bool IsCulturePublished(string culture); - // fixme doc + /// + /// Gets the date a culture was published. + /// DateTime GetDateCulturePublished(string culture); - // fixme doc + /// + /// Gets a value indicated whether a given culture is edited. + /// + /// + /// A culture is edited when it is not published, or when it is published but + /// it has changes. + /// bool IsCultureEdited(string culture); /// @@ -114,6 +122,21 @@ namespace Umbraco.Core.Models /// name, which must be get via the property. /// IReadOnlyDictionary PublishNames { get; } + + /// + /// Gets the available cultures. + /// + IEnumerable AvailableCultures { get; } + + /// + /// Gets the published cultures. + /// + IEnumerable PublishedCultures { get; } + + /// + /// Gets the edited cultures. + /// + IEnumerable EditedCultures { get; } // fixme - these two should move to some kind of service