diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index a9a468319e..e3bce53a3c 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -159,6 +159,9 @@ namespace Umbraco.Core.Composing public static IPublishedValueFallback PublishedValueFallback => _publishedValueFallback ?? Container.GetInstance() ?? new NoopPublishedValueFallback(); + public static IVariationContextAccessor VariationContextAccessor + => Container.GetInstance(); + #endregion } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 8d8991c396..620c040bfe 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -233,7 +233,7 @@ namespace Umbraco.Core.Migrations.Install private void CreateLanguageData() { - _database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefaultVariantLanguage = true }); + _database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }); } private void CreateContentChildTypeData() diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 0933c0cba7..4bb738a6b3 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -107,18 +107,35 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove, - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but it after shannon's, with a new target state, + Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state, Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // and provide a path out of the conflict state // resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ... Chain("{1350617A-4930-4D61-852F-E3AA9E692173}"); Chain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 + //Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove Chain("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0 Chain("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0 Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 - Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); - Chain("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}"); + //Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove + + Chain("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state + From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's + .CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next + // resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ... + + Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state + From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}") // and provide a path out of stephan's + .Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next + // resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ... + + //Chain("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}"); + Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); + From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}") + .Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); + // resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}... + //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs new file mode 100644 index 0000000000..f0d7c02b82 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + /// + /// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for + /// a given language. + /// + public class FallbackLanguage : MigrationBase + { + public FallbackLanguage(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) + AddColumn("fallbackLanguageId"); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs index dd5fe7c369..b965bc71d2 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var defaultId = int.MaxValue; foreach (var dto in dtos) { - if (dto.IsDefaultVariantLanguage) + if (dto.IsDefault) { defaultId = dto.Id; break; @@ -38,8 +38,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 // update, so that language with that id is now default and mandatory var updateDefault = Sql() .Update(u => u - .Set(x => x.IsDefaultVariantLanguage, true) - .Set(x => x.Mandatory, true)) + .Set(x => x.IsDefault, true) + .Set(x => x.IsMandatory, true)) .Where(x => x.Id == defaultId); Database.Execute(updateDefault); diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 7bf9e9b32c..c0d2fed839 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -4,34 +4,54 @@ using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { + /// + /// Represents a language. + /// public interface ILanguage : IEntity, IRememberBeingDirty { /// - /// Gets or sets the Iso Code for the Language + /// Gets or sets the ISO code of the language. /// [DataMember] string IsoCode { get; set; } /// - /// Gets or sets the Culture Name for the Language + /// Gets or sets the culture name of the language. /// [DataMember] string CultureName { get; set; } /// - /// Returns a object for the current Language + /// Gets the object for the language. /// [IgnoreDataMember] CultureInfo CultureInfo { get; } /// - /// Defines if this language is the default variant language when language variants are in use + /// Gets or sets a value indicating whether the language is the default language. /// - bool IsDefaultVariantLanguage { get; set; } + [DataMember] + bool IsDefault { get; set; } /// - /// If true, a variant node cannot be published unless this language variant is created + /// Gets or sets a value indicating whether the language is mandatory. /// - bool Mandatory { get; set; } + /// + /// When a language is mandatory, a multi-lingual document cannot be published + /// without that language being published, and unpublishing that language unpublishes + /// the entire document. + /// + [DataMember] + bool IsMandatory { get; set; } + + /// + /// Gets or sets the identifier of a fallback language. + /// + /// + /// The fallback language can be used in multi-lingual scenarios, to help + /// define fallback strategies when a value does not exist for a requested language. + /// + [DataMember] + int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index fa1c9dc826..940648c4b9 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Models private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; + private int? _fallbackLanguageId; public Language(string isoCode) { @@ -30,13 +31,12 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo IsoCodeSelector = ExpressionHelper.GetPropertyInfo(x => x.IsoCode); public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); - public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); - public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); + public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefault); + public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.IsMandatory); + public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguageId); } - /// - /// Gets or sets the Iso Code for the Language - /// + /// [DataMember] public string IsoCode { @@ -44,9 +44,7 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _isoCode, Ps.Value.IsoCodeSelector); } - /// - /// Gets or sets the Culture Name for the Language - /// + /// [DataMember] public string CultureName { @@ -54,22 +52,29 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector); } - /// - /// Returns a object for the current Language - /// + /// [IgnoreDataMember] public CultureInfo CultureInfo => CultureInfo.GetCultureInfo(IsoCode); - public bool IsDefaultVariantLanguage + /// + public bool IsDefault { get => _isDefaultVariantLanguage; set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, Ps.Value.IsDefaultVariantLanguageSelector); } - public bool Mandatory + /// + public bool IsMandatory { get => _mandatory; set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } + + /// + public int? FallbackLanguageId + { + get => _fallbackLanguageId; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector); + } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs new file mode 100644 index 0000000000..0434218555 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Manages the built-in fallback policies. + /// + public struct Fallback : IEnumerable + { + private readonly int[] _values; + + /// + /// Initializes a new instance of the struct with values. + /// + private Fallback(int[] values) + { + _values = values; + } + + /// + /// Gets an ordered set of fallback policies. + /// + /// + public static Fallback To(params int[] values) => new Fallback(values); + + /// + /// Do not fallback. + /// + public const int None = 0; + + /// + /// Fallback to default value. + /// + public const int DefaultValue = 1; + + /// + /// Gets the fallback to default value policy. + /// + public static Fallback ToDefaultValue => new Fallback(new[] { DefaultValue }); + + /// + /// Fallback to other languages. + /// + public const int Language = 2; + + /// + /// Gets the fallback to language policy. + /// + public static Fallback ToLanguage => new Fallback(new[] { Language }); + + /// + /// Fallback to tree ancestors. + /// + public const int Ancestors = 3; + + /// + /// Gets the fallback to tree ancestors policy. + /// + public static Fallback ToAncestors => new Fallback(new[] { Ancestors }); + + /// + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_values ?? Array.Empty()).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 8e1dcfd543..f30a53c8b6 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -1,37 +1,122 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Core.Models.PublishedContent +namespace Umbraco.Core.Models.PublishedContent { /// /// Provides a fallback strategy for getting values. /// - // fixme - IPublishedValueFallback is still WorkInProgress - // todo - properly document methods, etc - // todo - understand caching vs fallback (recurse etc) public interface IPublishedValueFallback { - // note that at property level, property.GetValue() does NOT implement fallback, and one has - // to get property.Value() or property.Value() to trigger fallback + /// + /// Tries to get a fallback value for a property. + /// + /// The property. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// It can only fallback at property level (no recurse). + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// Note that and may not be contextualized, + /// so the variant context should be used to contextualize them (see our default implementation in + /// the web project. + /// + bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value); - // this method is called whenever property.Value(culture, segment, defaultValue) is called, and - // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). + /// + /// Tries to get a fallback value for a property. + /// + /// The type of the value. + /// The property. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// It can only fallback at property level (no recurse). + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// + bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value); - object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); + /// + /// Tries to get a fallback value for a published element property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// It can only fallback at element level (no recurse). + /// + bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value); - // this method is called whenever property.Value(culture, segment, defaultValue) is called, and - // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). + /// + /// Tries to get a fallback value for a published element property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// It can only fallback at element level (no recurse). + /// + bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value); - T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); + /// + /// Tries to get a fallback value for a published content property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value); - // these methods to be called whenever getting the property value for the specified alias, culture and segment, - // either returned no property at all, or a property that does not HasValue for the specified culture and segment. - - object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); - - T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); - - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse); - - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse); + /// + /// Tries to get a fallback value for a published content property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs index 962148e138..a366742cc5 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs @@ -49,17 +49,24 @@ namespace Umbraco.Core.Models.PublishedContent /// The model types map. /// The actual Clr type. public static Type Map(Type type, Dictionary modelTypes) + => Map(type, modelTypes, false); + + public static Type Map(Type type, Dictionary modelTypes, bool dictionaryIsInvariant) { + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (!dictionaryIsInvariant) + modelTypes = new Dictionary(modelTypes, StringComparer.InvariantCultureIgnoreCase); + if (type is ModelType modelType) { - if (modelTypes.TryGetValue(modelType.ContentTypeAlias, out Type actualType)) + if (modelTypes.TryGetValue(modelType.ContentTypeAlias, out var actualType)) return actualType; throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); } if (type is ModelTypeArrayType arrayType) { - if (modelTypes.TryGetValue(arrayType.ContentTypeAlias, out Type actualType)) + if (modelTypes.TryGetValue(arrayType.ContentTypeAlias, out var actualType)) return actualType.MakeArrayType(); throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); } @@ -70,7 +77,7 @@ namespace Umbraco.Core.Models.PublishedContent if (def == null) throw new InvalidOperationException("panic"); - var args = type.GetGenericArguments().Select(x => Map(x, modelTypes)).ToArray(); + var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); return def.MakeGenericType(args); } @@ -81,7 +88,14 @@ namespace Umbraco.Core.Models.PublishedContent /// The model types map. /// The actual Clr type name. public static string MapToName(Type type, Dictionary map) + => MapToName(type, map, false); + + private static string MapToName(Type type, Dictionary map, bool dictionaryIsInvariant) { + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (!dictionaryIsInvariant) + map = new Dictionary(map, StringComparer.InvariantCultureIgnoreCase); + if (type is ModelType modelType) { if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName)) @@ -102,7 +116,7 @@ namespace Umbraco.Core.Models.PublishedContent if (def == null) throw new InvalidOperationException("panic"); - var args = type.GetGenericArguments().Select(x => MapToName(x, map)).ToArray(); + var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); var defFullName = def.FullName.Substring(0, def.FullName.IndexOf('`')); return defFullName + "<" + string.Join(", ", args) + ">"; } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index b99b4ad415..cd7b063d44 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -9,21 +9,45 @@ public class NoopPublishedValueFallback : IPublishedValueFallback { /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; + public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value) + { + value = default; + return false; + } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; + public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value) + { + value = default; + return false; + } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; + public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value) + { + value = default; + return false; + } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; + public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value) + { + value = default; + return false; + } /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) => defaultValue; + public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value) + { + value = default; + return false; + } /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue; + public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value) + { + value = default; + return false; + } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs new file mode 100644 index 0000000000..6710d79cc6 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Core.Models.PublishedContent +{ + public static class VariationContextAccessorExtensions + { + public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, ref string culture, ref string segment) + { + if (culture != null && segment != null) return; + + // use context values + var publishedVariationContext = variationContextAccessor?.VariationContext; + if (culture == null) culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; + if (segment == null) segment = variations.VariesBySegment() ? publishedVariationContext?.Segment : ""; + } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 08bd2a0582..488390f985 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -10,33 +10,51 @@ namespace Umbraco.Core.Persistence.Dtos { public const string TableName = Constants.DatabaseSchema.Tables.Language; + /// + /// Gets or sets the identifier of the language. + /// [Column("id")] [PrimaryKeyColumn(IdentitySeed = 2)] public short Id { get; set; } + /// + /// Gets or sets the ISO code of the language. + /// [Column("languageISOCode")] [Index(IndexTypes.UniqueNonClustered)] [NullSetting(NullSetting = NullSettings.Null)] [Length(14)] public string IsoCode { get; set; } + /// + /// Gets or sets the culture name of the language. + /// [Column("languageCultureName")] [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] public string CultureName { get; set; } /// - /// Defines if this language is the default variant language when language variants are in use + /// Gets or sets a value indicating whether the language is the default language. /// [Column("isDefaultVariantLang")] [Constraint(Default = "0")] - public bool IsDefaultVariantLanguage { get; set; } + public bool IsDefault { get; set; } /// - /// If true, a variant node cannot be published unless this language variant is created + /// Gets or sets a value indicating whether the language is mandatory. /// [Column("mandatory")] [Constraint(Default = "0")] - public bool Mandatory { get; set; } + public bool IsMandatory { get; set; } + + /// + /// Gets or sets the identifier of a fallback language. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index 30bd9c68ed..ad58c5b570 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -12,8 +12,9 @@ namespace Umbraco.Core.Persistence.Factories { CultureName = dto.CultureName, Id = dto.Id, - IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, - Mandatory = dto.Mandatory + IsDefault = dto.IsDefault, + IsMandatory = dto.IsMandatory, + FallbackLanguageId = dto.FallbackLanguageId }; // reset dirty initial properties (U4-1946) @@ -27,12 +28,15 @@ namespace Umbraco.Core.Persistence.Factories { CultureName = entity.CultureName, IsoCode = entity.IsoCode, - IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, - Mandatory = entity.Mandatory + IsDefault = entity.IsDefault, + IsMandatory = entity.IsMandatory, + FallbackLanguageId = entity.FallbackLanguageId }; if (entity.HasIdentity) + { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); + } return dto; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index ae5d9ae8b8..2b3674700b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -74,7 +74,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConvertFromDto); + var dtos = Database.Fetch(sql); + return dtos.Select(ConvertFromDto).ToList(); } #endregion @@ -115,10 +116,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return list; } - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } + protected override Guid NodeObjectTypeId => throw new NotImplementedException(); #endregion @@ -133,15 +131,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ((EntityBase) entity).AddingEntity(); // deal with entity becoming the new default entity - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) { // set all other entities to non-default // safe (no race cond) because the service locks languages var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefaultVariantLanguage, false)); + .Update(u => u.Set(x => x.IsDefault, false)); Database.Execute(setAllDefaultToFalse); } -; + + // fallback cycles are detected at service level + // insert var dto = LanguageFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); @@ -157,14 +157,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ((EntityBase) entity).UpdatingEntity(); - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) { // deal with entity becoming the new default entity // set all other entities to non-default // safe (no race cond) because the service locks languages var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefaultVariantLanguage, false)); + .Update(u => u.Set(x => x.IsDefault, false)); Database.Execute(setAllDefaultToFalse); } else @@ -174,13 +174,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var selectDefaultId = Sql() .Select(x => x.Id) .From() - .Where(x => x.IsDefaultVariantLanguage); + .Where(x => x.IsDefault); var defaultId = Database.ExecuteScalar(selectDefaultId); if (entity.Id == defaultId) throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); } + // fallback cycles are detected at service level + // update var dto = LanguageFactory.BuildDto(entity); Database.Update(dto); @@ -195,12 +197,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var selectDefaultId = Sql() .Select(x => x.Id) .From() - .Where(x => x.IsDefaultVariantLanguage); + .Where(x => x.IsDefault); var defaultId = Database.ExecuteScalar(selectDefaultId); if (entity.Id == defaultId) throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); + // We need to remove any references to the language if it's being used as a fall-back from other ones + var clearFallbackLanguage = Sql() + .Update(u => u + .Set(x => x.FallbackLanguageId, null)) + .Where(x => x.FallbackLanguageId == entity.Id); + + Database.Execute(clearFallbackLanguage); + // delete base.PersistDeletedItem(entity); } @@ -212,7 +222,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = LanguageFactory.BuildEntity(dto); return entity; } - + public ILanguage GetByIsoCode(string isoCode) { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way @@ -271,7 +281,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // get all cached, non-cloned var languages = TypedCachePolicy.GetAllCached(PerformGetAll).ToList(); - var language = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage); + var language = languages.FirstOrDefault(x => x.IsDefault); if (language != null) return language; // this is an anomaly, the service/repo should ensure it cannot happen diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 10784e12d6..a849813b13 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1034,7 +1034,7 @@ namespace Umbraco.Core.Services.Implement var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published if (!cannotBePublished) { - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published } diff --git a/src/Umbraco.Core/Services/Implement/LocalizationService.cs b/src/Umbraco.Core/Services/Implement/LocalizationService.cs index e136cb5a68..49a764b533 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizationService.cs @@ -363,6 +363,16 @@ namespace Umbraco.Core.Services.Implement // write-lock languages to guard against race conds when dealing with default language scope.WriteLock(Constants.Locks.Languages); + // look for cycles - within write-lock + if (language.FallbackLanguageId.HasValue) + { + var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x); + if (!languages.ContainsKey(language.FallbackLanguageId.Value)) + throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id."); + if (CreatesCycle(language, languages)) + throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle."); + } + var saveEventArgs = new SaveEventArgs(language); if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs)) { @@ -380,6 +390,20 @@ namespace Umbraco.Core.Services.Implement } } + private bool CreatesCycle(ILanguage language, IDictionary languages) + { + // a new language is not referenced yet, so cannot be part of a cycle + if (!language.HasIdentity) return false; + + var id = language.FallbackLanguageId; + while (true) // assuming languages does not already contains a cycle, this must end + { + if (!id.HasValue) return false; // no fallback means no cycle + if (id.Value == language.Id) return true; // back to language = cycle! + id = languages[id.Value].FallbackLanguageId; // else keep chaining + } + } + /// /// Deletes a by removing it (but not its usages) from the db /// @@ -399,8 +423,7 @@ namespace Umbraco.Core.Services.Implement return; } - //NOTE: There isn't any constraints in the db, so possible references aren't deleted - + // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); deleteEventArgs.CanCancel = false; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 496a66137a..6148162c18 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -362,6 +362,7 @@ + @@ -391,12 +392,14 @@ + + diff --git a/src/Umbraco.Tests/Composing/TypeFinderTests.cs b/src/Umbraco.Tests/Composing/TypeFinderTests.cs index 955f6f94c8..a8624e8871 100644 --- a/src/Umbraco.Tests/Composing/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeFinderTests.cs @@ -90,7 +90,7 @@ namespace Umbraco.Tests.Composing Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree] typesFound = TypeFinder.FindClassesWithAttribute(new[] { typeof (UmbracoContext).Assembly }); - Assert.AreEqual(21, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(22, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] } private static ProfilingLogger GetTestProfilingLogger() diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config index 37f2723c81..4040412603 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config @@ -1,114 +1,114 @@  -
- - settings - - - - views/dashboard/settings/settingsdashboardintro.html - - - views/dashboard/settings/settingsdashboardvideos.html - - -
+
+ + settings + + + + views/dashboard/settings/settingsdashboardintro.html + + + views/dashboard/settings/settingsdashboardvideos.html + + + + dashboard/ExamineManagement.ascx + +
-
- - developer - - - - views/dashboard/developer/developerdashboardintro.html - - - views/dashboard/developer/developerdashboardvideos.html - - - - dashboard/ExamineManagement.ascx - -
+
+ + developer + + + + views/dashboard/developer/developerdashboardintro.html + + + views/dashboard/developer/developerdashboardvideos.html + + +
-
- - media - - - - views/dashboard/media/mediafolderbrowser.html - - - - - admin - - - views/dashboard/media/mediadashboardintro.html - - - views/dashboard/media/desktopmediauploader.html - - - views/dashboard/media/mediadashboardvideos.html - - -
+
+ + media + + + + views/dashboard/media/mediafolderbrowser.html + + + + + admin + + + views/dashboard/media/mediadashboardintro.html + + + views/dashboard/media/desktopmediauploader.html + + + views/dashboard/media/mediadashboardvideos.html + + +
-
- - translator - hello - world - - - content - - - - admin - - - views/dashboard/default/startupdashboardintro.html - - - views/dashboard/default/startupdashboardkits.html +
- editor - writer + translator + hello + world - - - views/dashboard/default/startupdashboardvideos.html - - - - dashboard/latestEdits.ascx - - - - views/dashboard/changepassword.html - - -
+ + content + + + + admin + + + views/dashboard/default/startupdashboardintro.html + + + views/dashboard/default/startupdashboardkits.html + + editor + writer + + + + views/dashboard/default/startupdashboardvideos.html + + + + dashboard/latestEdits.ascx + + + + views/dashboard/changepassword.html + + +
-
- - default - member - - - - views/dashboard/members/membersdashboardintro.html - - - members/membersearch.ascx - - - views/dashboard/members/membersdashboardvideos.html - - -
+
+ + default + member + + + + views/dashboard/members/membersdashboardintro.html + + + members/membersearch.ascx + + + views/dashboard/members/membersdashboardvideos.html + + +
diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs index ed3b36c91f..862dfb3dc2 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs @@ -66,10 +66,21 @@ namespace Umbraco.Tests.Configurations.DashboardSettings [Test] public void Test_Section_Tabs() { - Assert.AreEqual(1, SettingsSection.Sections.ElementAt(0).Tabs.Count()); - Assert.AreEqual(2, SettingsSection.Sections.ElementAt(1).Tabs.Count()); + //Element 0 Alias "StartupSettingsDashboardSection" + Assert.AreEqual(2, SettingsSection.Sections.ElementAt(0).Tabs.Count()); + + //Element 1 Alias "StartupDeveloperDashboardSection" + Assert.AreEqual(1, SettingsSection.Sections.ElementAt(1).Tabs.Count()); + + //Element 2 Alias "StartupMediaDashboardSection" + Assert.AreEqual(2, SettingsSection.Sections.ElementAt(2).Tabs.Count()); + + //Element 3 Alias "StartupDashboardSection" Assert.AreEqual(3, SettingsSection.Sections.ElementAt(3).Tabs.Count()); + //Element 4 Alias "StartupMemberDashboardSection" + Assert.AreEqual(1, SettingsSection.Sections.ElementAt(4).Tabs.Count()); + } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index 945dd29a49..a063d2e387 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -47,6 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo("English (United States)")); Assert.That(language.IsoCode, Is.EqualTo("en-US")); + Assert.That(language.FallbackLanguageId, Is.Null); } } @@ -61,7 +62,8 @@ namespace Umbraco.Tests.Persistence.Repositories var au = CultureInfo.GetCultureInfo("en-AU"); var language = (ILanguage)new Language(au.Name) { - CultureName = au.DisplayName + CultureName = au.DisplayName, + FallbackLanguageId = 1 }; repository.Save(language); @@ -73,6 +75,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo(au.DisplayName)); Assert.That(language.IsoCode, Is.EqualTo(au.Name)); + Assert.That(language.FallbackLanguageId, Is.EqualTo(1)); } } @@ -182,14 +185,15 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var languageBR = new Language("pt-BR") {CultureName = "pt-BR"}; + var languageBR = new Language("pt-BR") { CultureName = "pt-BR" }; repository.Save(languageBR); // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); - Assert.IsFalse(languageBR.Mandatory); + Assert.IsFalse(languageBR.IsDefault); + Assert.IsFalse(languageBR.IsMandatory); + Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -203,14 +207,39 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true }; repository.Save(languageBR); // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.IsTrue(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageBR.Mandatory); + Assert.IsTrue(languageBR.IsDefault); + Assert.IsTrue(languageBR.IsMandatory); + Assert.IsNull(languageBR.FallbackLanguageId); + } + } + + [Test] + public void Can_Perform_Add_On_LanguageRepository_With_Fallback_Language() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + // Act + var languageBR = new Language("pt-BR") + { + CultureName = "pt-BR", + FallbackLanguageId = 1 + }; + repository.Save(languageBR); + + // Assert + Assert.That(languageBR.HasIdentity, Is.True); + Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 + Assert.That(languageBR.FallbackLanguageId, Is.EqualTo(1)); } } @@ -223,24 +252,22 @@ namespace Umbraco.Tests.Persistence.Repositories { var repository = CreateRepository(provider); - var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true }; repository.Save(languageBR); var languageEN = new Language("en-AU") { CultureName = "en-AU" }; repository.Save(languageEN); - Assert.IsTrue(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageBR.Mandatory); + Assert.IsTrue(languageBR.IsDefault); + Assert.IsTrue(languageBR.IsMandatory); // Act - - var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefault = true, IsMandatory = true }; repository.Save(languageNZ); languageBR = repository.Get(languageBR.Id); // Assert - - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageNZ.IsDefaultVariantLanguage); + Assert.IsFalse(languageBR.IsDefault); + Assert.IsTrue(languageNZ.IsDefault); } } @@ -257,6 +284,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = repository.Get(5); language.IsoCode = "pt-BR"; language.CultureName = "pt-BR"; + language.FallbackLanguageId = 1; repository.Save(language); @@ -266,6 +294,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageUpdated, Is.Not.Null); Assert.That(languageUpdated.IsoCode, Is.EqualTo("pt-BR")); Assert.That(languageUpdated.CultureName, Is.EqualTo("pt-BR")); + Assert.That(languageUpdated.FallbackLanguageId, Is.EqualTo(1)); } } @@ -289,6 +318,30 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Delete_On_LanguageRepository_With_Language_Used_As_Fallback() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + // Add language to delete as a fall-back language to another one + var repository = CreateRepository(provider); + var languageToFallbackFrom = repository.Get(5); + languageToFallbackFrom.FallbackLanguageId = 2; // fall back to #2 (something we can delete) + repository.Save(languageToFallbackFrom); + + // delete #2 + var languageToDelete = repository.Get(2); + repository.Delete(languageToDelete); + + var exists = repository.Exists(2); + + // has been deleted + Assert.That(exists, Is.False); + } + } + [Test] public void Can_Perform_Exists_On_LanguageRepository() { @@ -314,8 +367,10 @@ namespace Umbraco.Tests.Persistence.Repositories base.TearDown(); } - public void CreateTestData() + private void CreateTestData() { + //Id 1 is en-US - when Umbraco is installed + var languageDK = new Language("da-DK") { CultureName = "da-DK" }; ServiceContext.LocalizationService.Save(languageDK);//Id 2 diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs new file mode 100644 index 0000000000..4e98aea000 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Tests.Testing; +using Umbraco.Web; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] + public class PublishedContentLanguageVariantTests : PublishedContentSnapshotTestBase + { + protected override void Compose() + { + base.Compose(); + + Container.RegisterSingleton(_ => GetServiceContext()); + } + + protected ServiceContext GetServiceContext() + { + var serviceContext = TestObjects.GetServiceContextMock(Container); + MockLocalizationService(serviceContext); + return serviceContext; + } + + private static void MockLocalizationService(ServiceContext serviceContext) + { + // Set up languages. + // Spanish falls back to English and Italian to Spanish (and then to English). + // French has no fall back. + // Danish, Swedish and Norweigan create an invalid loop. + var languages = new List + { + new Language("en-US") { Id = 1, CultureName = "English", IsDefault = true }, + new Language("fr") { Id = 2, CultureName = "French" }, + new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 }, + new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 }, + new Language("de") { Id = 5, CultureName = "German" }, + new Language("da") { Id = 6, CultureName = "Danish", FallbackLanguageId = 8 }, + new Language("sv") { Id = 7, CultureName = "Swedish", FallbackLanguageId = 6 }, + new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 }, + new Language("nl") { Id = 9, CultureName = "Dutch", FallbackLanguageId = 1 } + }; + + var localizationService = Mock.Get(serviceContext.LocalizationService); + localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); + localizationService.Setup(x => x.GetLanguageById(It.IsAny())) + .Returns((int id) => languages.SingleOrDefault(y => y.Id == id)); + localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) + .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); + } + + internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) + { + var props = new[] + { + factory.CreatePropertyType("prop1", 1), + factory.CreatePropertyType("welcomeText", 1), + factory.CreatePropertyType("welcomeText2", 1), + }; + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); + + var prop1 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText", + }; + prop1.SetSourceValue("en-US", "Welcome", true); + prop1.SetValue("en-US", "Welcome", true); + prop1.SetSourceValue("de", "Willkommen"); + prop1.SetValue("de", "Willkommen"); + prop1.SetSourceValue("nl", "Welkom"); + prop1.SetValue("nl", "Welkom"); + + var prop2 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText2", + }; + prop2.SetSourceValue("en-US", "Welcome", true); + prop2.SetValue("en-US", "Welcome", true); + + var prop3 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText", + }; + prop3.SetSourceValue("en-US", "Welcome", true); + prop3.SetValue("en-US", "Welcome", true); + + var item1 = new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlSegment = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new[] { 2 }, + Properties = new Collection + { + prop1, prop2 + } + }; + + var item2 = new SolidPublishedContent(contentType1) + { + Id = 2, + SortOrder = 0, + Name = "Content 2", + UrlSegment = "content-2", + Path = "/1/2", + Level = 2, + Url = "/content-1/content-2", + ParentId = 1, + ChildIds = new int[] { }, + Properties = new Collection + { + prop3 + } + }; + + item1.Children = new List { item2 }; + item2.Parent = item1; + + cache.Add(item1); + cache.Add(item2); + } + + [Test] + public void Can_Get_Content_For_Populated_Requested_Language() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "en-US"); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_For_Populated_Requested_Non_Default_Language() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "de"); + Assert.AreEqual("Willkommen", value); + } + + [Test] + public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "fr"); + Assert.IsNull(value); + } + + [Test] + public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "es"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "es", fallback: Fallback.ToLanguage); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors)); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "no", fallback: Fallback.ToLanguage); + Assert.IsNull(value); + } + + [Test] + public void Do_Not_Get_Content_Recursively_Unless_Requested() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText2"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_Recursively() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText2", fallback: Fallback.ToAncestors); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_With_Recursive_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallback: Fallback.To(Fallback.Ancestors, Fallback.Language)); + + // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. + Assert.AreEqual("Welkom", value); + } + + [Test] + public void Can_Get_Content_With_Fallback_Language_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallback: Fallback.ToLanguage); + + // No Dutch value is directly assigned. Check has fallen back to English value from language variant. + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Throws_For_Non_Supported_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + Assert.Throws(() => content.Value("welcomeText", "nl", fallback: Fallback.To(999))); + } + + [Test] + public void Can_Fallback_To_Default_Value() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + + // no Dutch value is assigned, so getting null + var value = content.Value("welcomeText", "nl"); + Assert.IsNull(value); + + // even if we 'just' provide a default value + value = content.Value("welcomeText", "nl", defaultValue: "woop"); + Assert.IsNull(value); + + // but it works with proper fallback settings + value = content.Value("welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop"); + Assert.AreEqual("woop", value); + } + + [Test] + public void Can_Have_Custom_Default_Value() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + + // hack the value, pretend the converter would return something + var prop = content.GetProperty("welcomeText") as SolidPublishedPropertyWithLanguageVariants; + Assert.IsNotNull(prop); + prop.SetValue("nl", "nope"); // HasValue false but getting value returns this + + // there is an EN value + var value = content.Value("welcomeText", "en-US"); + Assert.AreEqual("Welcome", value); + + // there is no NL value and we get the 'converted' value + value = content.Value("welcomeText", "nl"); + Assert.AreEqual("nope", value); + + // but it works with proper fallback settings + value = content.Value("welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop"); + Assert.AreEqual("woop", value); + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 101c7827c3..22965ac141 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -1,91 +1,94 @@ -using System; +using System.Collections.ObjectModel; using System.Linq; -using System.Collections.ObjectModel; -using System.Web.Routing; -using Moq; using NUnit.Framework; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using Umbraco.Core.Composing; -using Current = Umbraco.Core.Composing.Current; -using LightInject; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.Accessors; namespace Umbraco.Tests.PublishedContent { [TestFixture] [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] - public class PublishedContentMoreTests : PublishedContentTestBase + public class PublishedContentMoreTests : PublishedContentSnapshotTestBase { - // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet - // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx - // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx - - public override void SetUp() + internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) { - base.SetUp(); + var props = new[] + { + factory.CreatePropertyType("prop1", 1), + }; + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); + var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), props); + var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), props); - var umbracoContext = GetUmbracoContext(); - Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; - } + cache.Add(new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlSegment = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - protected override void Compose() - { - base.Compose(); + cache.Add(new SolidPublishedContent(contentType2) + { + Id = 2, + SortOrder = 1, + Name = "Content 2", + UrlSegment = "content-2", + Path = "/2", + Level = 1, + Url = "/content-2", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); - } - - protected override TypeLoader CreatePluginManager(IServiceFactory f) - { - var pluginManager = base.CreatePluginManager(f); - - // this is so the model factory looks into the test assembly - pluginManager.AssembliesToScan = pluginManager.AssembliesToScan - .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) - .ToList(); - - return pluginManager; - } - - private UmbracoContext GetUmbracoContext() - { - RouteData routeData = null; - - var publishedSnapshot = CreatePublishedSnapshot(); - - var publishedSnapshotService = new Mock(); - publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); - - var globalSettings = TestObjects.GetGlobalSettings(); - - var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; - var umbracoContext = new UmbracoContext( - httpContext, - publishedSnapshotService.Object, - new WebSecurity(httpContext, Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), - Enumerable.Empty(), - globalSettings, - new TestVariationContextAccessor()); - - return umbracoContext; - } - - public override void TearDown() - { - base.TearDown(); - - Current.Reset(); + cache.Add(new SolidPublishedContent(contentType2Sub) + { + Id = 3, + SortOrder = 2, + Name = "Content 2Sub", + UrlSegment = "content-2sub", + Path = "/3", + Level = 1, + Url = "/content-2sub", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); } [Test] @@ -197,95 +200,5 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1, result[0].Id); Assert.AreEqual(2, result[1].Id); } - - private static SolidPublishedSnapshot CreatePublishedSnapshot() - { - var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); - - var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); - var caches = new SolidPublishedSnapshot(); - var cache = caches.InnerContentCache; - - var props = new[] - { - factory.CreatePropertyType("prop1", 1), - }; - - var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); - var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), props); - var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), props); - - cache.Add(new SolidPublishedContent(contentType1) - { - Id = 1, - SortOrder = 0, - Name = "Content 1", - UrlSegment = "content-1", - Path = "/1", - Level = 1, - Url = "/content-1", - ParentId = -1, - ChildIds = new int[] {}, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2) - { - Id = 2, - SortOrder = 1, - Name = "Content 2", - UrlSegment = "content-2", - Path = "/2", - Level = 1, - Url = "/content-2", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2Sub) - { - Id = 3, - SortOrder = 2, - Name = "Content 2Sub", - UrlSegment = "content-2sub", - Path = "/3", - Level = 1, - Url = "/content-2sub", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - return caches; - } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs new file mode 100644 index 0000000000..623472a023 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Collections.ObjectModel; +using System.Web.Routing; +using Moq; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; +using Umbraco.Core.Composing; +using Current = Umbraco.Core.Composing.Current; +using LightInject; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects.Accessors; + +namespace Umbraco.Tests.PublishedContent +{ + public abstract class PublishedContentSnapshotTestBase : PublishedContentTestBase + { + // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet + // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx + // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx + + public override void SetUp() + { + base.SetUp(); + + var umbracoContext = GetUmbracoContext(); + Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; + } + + protected override void Compose() + { + base.Compose(); + + Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); + } + + protected override TypeLoader CreatePluginManager(IServiceFactory f) + { + var pluginManager = base.CreatePluginManager(f); + + // this is so the model factory looks into the test assembly + pluginManager.AssembliesToScan = pluginManager.AssembliesToScan + .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) + .ToList(); + + return pluginManager; + } + + private UmbracoContext GetUmbracoContext() + { + RouteData routeData = null; + + var publishedSnapshot = CreatePublishedSnapshot(); + + var publishedSnapshotService = new Mock(); + publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); + + var globalSettings = TestObjects.GetGlobalSettings(); + + var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; + var umbracoContext = new UmbracoContext( + httpContext, + publishedSnapshotService.Object, + new WebSecurity(httpContext, Current.Services.UserService, globalSettings), + TestObjects.GetUmbracoSettings(), + Enumerable.Empty(), + globalSettings, + new TestVariationContextAccessor()); + + return umbracoContext; + } + + public override void TearDown() + { + base.TearDown(); + + Current.Reset(); + } + + private SolidPublishedSnapshot CreatePublishedSnapshot() + { + var dataTypeService = new TestObjects.TestDataTypeService( + new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); + + var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); + var caches = new SolidPublishedSnapshot(); + var cache = caches.InnerContentCache; + PopulateCache(factory, cache); + return caches; + } + + internal abstract void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache); + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 3697ffa586..14dae46bcb 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -38,7 +38,7 @@ namespace Umbraco.Tests.PublishedContent var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(logger)) { Id = 1}, + new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, new DataType(new RichTextPropertyEditor(logger)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, @@ -338,11 +338,11 @@ namespace Umbraco.Tests.PublishedContent } [Test] - public void GetPropertyValueRecursiveTest() + public void Get_Property_Value_Recursive() { var doc = GetNode(1174); - var rVal = doc.Value("testRecursive", recurse: true); - var nullVal = doc.Value("DoNotFindThis", recurse: true); + var rVal = doc.Value("testRecursive", fallback: Fallback.ToAncestors); + var nullVal = doc.Value("DoNotFindThis", fallback: Fallback.ToAncestors); Assert.AreEqual("This is the recursive val", rVal); Assert.AreEqual(null, nullVal); } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 756b775e46..efd1c6ae8b 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -248,7 +248,7 @@ namespace Umbraco.Tests.PublishedContent #endregion } - class SolidPublishedProperty : IPublishedProperty + internal class SolidPublishedProperty : IPublishedProperty { public PublishedPropertyType PropertyType { get; set; } public string Alias { get; set; } @@ -257,10 +257,86 @@ namespace Umbraco.Tests.PublishedContent public bool SolidHasValue { get; set; } public object SolidXPathValue { get; set; } - 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; + public virtual object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue; + public virtual object GetValue(string culture = null, string segment = null) => SolidValue; + public virtual object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue; + public virtual bool HasValue(string culture = null, string segment = null) => SolidHasValue; + } + + internal class SolidPublishedPropertyWithLanguageVariants : SolidPublishedProperty + { + private readonly IDictionary _solidSourceValues = new Dictionary(); + private readonly IDictionary _solidValues = new Dictionary(); + private readonly IDictionary _solidXPathValues = new Dictionary(); + + public override object GetSourceValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetSourceValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture) ? _solidSourceValues[culture] : null; + } + + public override object GetValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetValue(culture, segment); + } + + return _solidValues.ContainsKey(culture) ? _solidValues[culture] : null; + } + + public override object GetXPathValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetXPathValue(culture, segment); + } + + return _solidXPathValues.ContainsKey(culture) ? _solidXPathValues[culture] : null; + } + + public override bool HasValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.HasValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture); + } + + public void SetSourceValue(string culture, object value, bool defaultValue = false) + { + _solidSourceValues.Add(culture, value); + if (defaultValue) + { + SolidSourceValue = value; + SolidHasValue = true; + } + } + + public void SetValue(string culture, object value, bool defaultValue = false) + { + _solidValues.Add(culture, value); + if (defaultValue) + { + SolidValue = value; + SolidHasValue = true; + } + } + + public void SetXPathValue(string culture, object value, bool defaultValue = false) + { + _solidXPathValues.Add(culture, value); + if (defaultValue) + { + SolidXPathValue = value; + } + } } [PublishedModel("ContentType2")] diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index e9d855535b..f187b8b70d 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2489,7 +2489,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; + var langUk = new Language("en-UK") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2524,7 +2524,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; + var langUk = new Language("en-UK") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2562,7 +2562,7 @@ namespace Umbraco.Tests.Services var languageService = ServiceContext.LocalizationService; //var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; - var langXx = new Language("pt-PT") { IsDefaultVariantLanguage = true }; + var langXx = new Language("pt-PT") { IsDefault = true }; var langFr = new Language("fr-FR"); var langUk = new Language("en-UK"); var langDe = new Language("de-DE"); diff --git a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs index 033fa08d4a..a61b208c4d 100644 --- a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs @@ -192,6 +192,20 @@ namespace Umbraco.Tests.Services Assert.Null(language); } + [Test] + public void Can_Delete_Language_Used_As_Fallback() + { + var danish = ServiceContext.LocalizationService.GetLanguageByIsoCode("da-DK"); + var norwegian = new Language("nb-NO") { CultureName = "Norwegian", FallbackLanguageId = danish.Id }; + ServiceContext.LocalizationService.Save(norwegian, 0); + var languageId = danish.Id; + + ServiceContext.LocalizationService.Delete(danish); + + var language = ServiceContext.LocalizationService.GetLanguageById(languageId); + Assert.Null(language); + } + [Test] public void Can_Create_DictionaryItem_At_Root() { @@ -362,21 +376,21 @@ namespace Umbraco.Tests.Services { var localizationService = ServiceContext.LocalizationService; var language = new Core.Models.Language("en-AU"); - language.IsDefaultVariantLanguage = true; + language.IsDefault = true; localizationService.Save(language); var result = localizationService.GetLanguageById(language.Id); - Assert.IsTrue(result.IsDefaultVariantLanguage); + Assert.IsTrue(result.IsDefault); var language2 = new Core.Models.Language("en-NZ"); - language2.IsDefaultVariantLanguage = true; + language2.IsDefault = true; localizationService.Save(language2); var result2 = localizationService.GetLanguageById(language2.Id); //re-get result = localizationService.GetLanguageById(language.Id); - Assert.IsTrue(result2.IsDefaultVariantLanguage); - Assert.IsFalse(result.IsDefaultVariantLanguage); + Assert.IsTrue(result2.IsDefault); + Assert.IsFalse(result.IsDefault); } [Test] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index a3f02b2fdc..a0822efc15 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -123,6 +123,8 @@ + + diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 9628cddfac..9f9c4fff01 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -16,13 +16,13 @@ "tests" ], "dependencies": { - "angular": "~1.7.2", - "angular-cookies": "~1.7.2", - "angular-sanitize": "~1.7.2", - "angular-touch": "~1.7.2", - "angular-route": "~1.7.2", - "angular-animate": "~1.7.2", - "angular-i18n": "~1.7.2", + "angular": "~1.7.4", + "angular-cookies": "~1.7.4", + "angular-sanitize": "~1.7.4", + "angular-touch": "~1.7.4", + "angular-route": "~1.7.4", + "angular-animate": "~1.7.4", + "angular-i18n": "~1.7.4", "signalr": "^2.2.1", "typeahead.js": "~0.10.5", "underscore": "~1.9.1", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 71d7da32eb..aec667077c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -3,7 +3,7 @@ function ContentEditController($rootScope, $scope, $routeParams, $q, $window, appState, contentResource, entityResource, navigationService, notificationsService, - serverValidationManager, contentEditingHelper, treeService, formHelper, umbRequestHelper, + serverValidationManager, contentEditingHelper, treeService, formHelper, umbRequestHelper, editorState, $http, eventsService, relationResource, overlayService) { var evts = []; @@ -56,11 +56,11 @@ } bindEvents(); - - resetVariantFlags(); + + resetVariantFlags(); } - + /** * This will reset isDirty flags if save is true. * When working with multiple variants, this will set the save/publish flags of each one to false. @@ -85,7 +85,7 @@ $scope.content.variants[0].publish = false; } } - + /** Returns true if the save/publish dialog should be shown when pressing the button */ function showSaveOrPublishDialog() { return $scope.content.variants.length > 1; @@ -144,7 +144,7 @@ */ function createButtons(content, app) { - // only create the save/publish/preview buttons if the + // only create the save/publish/preview buttons if the // content app is "Conent" if(app && app.alias !== "umbContent" && app.alias !== "umbInfo") { $scope.defaultButton = null; @@ -173,7 +173,7 @@ schedulePublish: $scope.schedule } }); - + $scope.defaultButton = buttons.defaultButton; $scope.subButtons = buttons.subButtons; $scope.page.showPreviewButton = true; @@ -203,7 +203,7 @@ if (infiniteMode || !path) { return; } - + if (!$scope.content.isChildOfListView) { navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { $scope.page.menu.currentNode = syncArgs.node; @@ -214,7 +214,7 @@ //it's a child item, just sync the ui node to the parent navigationService.syncTree({ tree: $scope.treeAlias, path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node // from the server so that we can load in the actions menu. umbRequestHelper.resourcePromise( $http.get(content.treeNodeUrl), @@ -224,9 +224,89 @@ } } + function checkValidility(){ + //Get all controls from the 'contentForm' + var allControls = $scope.contentForm.$getControls(); + + //An array to store items in when we find child form fields (no matter how many deep nested forms) + var childFieldsToMarkAsValid = []; + + //Exclude known formControls 'contentHeaderForm' and 'tabbedContentForm' + //Check property - $name === "contentHeaderForm" + allControls = _.filter(allControls, function(obj){ + return obj.$name !== 'contentHeaderForm' && obj.$name !== 'tabbedContentForm' && obj.hasOwnProperty('$submitted'); + }); + + for (var i = 0; i < allControls.length; i++) { + var nestedForm = allControls[i]; + + //Get Nested Controls of this form in the loop + var nestedFormControls = nestedForm.$getControls(); + + //Need to recurse through controls (could be more nested forms) + childFieldsToMarkAsValid = recurseFormControls(nestedFormControls, childFieldsToMarkAsValid); + } + + return childFieldsToMarkAsValid; + } + + //Controls is the + function recurseFormControls(controls, array){ + + //Loop over the controls + for (var i = 0; i < controls.length; i++) { + var controlItem = controls[i]; + + //Check if the controlItem has a property '' + if(controlItem.hasOwnProperty('$submitted')){ + //This item is a form - so lets get the child controls of it & recurse again + var childFormControls = controlItem.$getControls(); + recurseFormControls(childFormControls, array); + } + else { + //We can assume its a field on a form + if(controlItem.hasOwnProperty('$error')){ + //Set the validlity of the error/s to be valid + //String of keys of error invalid messages + var errorKeys = []; + + for(var key in controlItem.$error){ + errorKeys.push(key); + controlItem.$setValidity(key, true); + } + + //Create a basic obj - storing the control item & the error keys + var obj = { 'control': controlItem, 'errorKeys': errorKeys }; + + //Push the updated control into the array - so we can set them back + array.push(obj); + } + } + } + return array; + } + + function resetNestedFieldValiation(array){ + for (var i = 0; i < array.length; i++) { + var item = array[i]; + //Item is an object containing two props + //'control' (obj) & 'errorKeys' (string array) + var fieldControl = item.control; + var fieldErrorKeys = item.errorKeys; + + for(var i = 0; i < fieldErrorKeys.length; i++) { + fieldControl.$setValidity(fieldErrorKeys[i], false); + } + } + } + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { + + //Used to check validility of nested form - coming from Content Apps mostly + //Set them all to be invalid + var fieldsToRollback = checkValidility(); eventsService.emit("content.saving", { content: $scope.content, action: args.action }); return contentEditingHelper.contentEditorPerformSave({ @@ -236,17 +316,17 @@ action: args.action, showNotifications: args.showNotifications }).then(function (data) { - //success + //success init($scope.content); - syncTreeNode($scope.content, data.path); eventsService.emit("content.saved", { content: $scope.content, action: args.action }); + resetNestedFieldValiation(fieldsToRollback); + return $q.when(data); }, function (err) { - syncTreeNode($scope.content, $scope.content.path); //error @@ -254,6 +334,8 @@ editorState.set($scope.content); } + resetNestedFieldValiation(fieldsToRollback); + return $q.reject(err); }); } @@ -388,7 +470,7 @@ overlayService.close(); } }; - + overlayService.open(dialog); } } @@ -551,9 +633,9 @@ if (!$scope.busy) { - // Chromes popup blocker will kick in if a window is opened + // Chromes popup blocker will kick in if a window is opened // without the initial scoped request. This trick will fix that. - // + // var previewWindow = $window.open('preview/?init=true', 'umbpreview'); // Build the correct path so both /#/ and #/ work. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index 46d0a1c5c1..a3a212a603 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -186,14 +186,15 @@ } else { vm.openVariants[editorIndex] = variant.language.culture; } - + } //then assign the variant to a view model to the content app var contentApp = _.find(variant.apps, function (a) { return a.alias === "umbContent"; }); - contentApp.viewModel = variant; + + contentApp.viewModel = _.omit(variant, 'apps'); // make sure the same app it set to active in the new variant if(activeAppAlias) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 35e9005cc6..eaf67bcb91 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -3,7 +3,7 @@ * @name umbraco.directives.directive:valFormManager * @restrict A * @require formController -* @description Used to broadcast an event to all elements inside this one to notify that form validation has +* @description Used to broadcast an event to all elements inside this one to notify that form validation has * changed. If we don't use this that means you have to put a watch for each directive on a form's validation * changing which would result in much higher processing. We need to actually watch the whole $error collection of a form * because just watching $valid or $invalid doesn't acurrately trigger form validation changing. @@ -19,7 +19,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var SAVED_EVENT_NAME = "formSubmitted"; return { - require: ["form", "^^?valFormManager"], + require: ["form", "^^?valFormManager", "^^?valSubView"], restrict: "A", controller: function($scope) { //This exposes an API for direct use with this directive @@ -46,9 +46,15 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location }, link: function (scope, element, attr, ctrls) { + function notifySubView() { + if (subView){ + subView.valStatusChanged({ form: formCtrl, showValidation: scope.showValidation }); + } + } + var formCtrl = ctrls[0]; var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null; - + var subView = ctrls.length > 1 ? ctrls[2] : null; var labels = {}; var labelKeys = [ @@ -83,7 +89,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location return sum; }, function (e) { scope.$broadcast("valStatusChanged", { form: formCtrl }); - + + notifySubView(); + //find all invalid elements' .control-group's and apply the error class var inError = element.find(".control-group .ng-invalid").closest(".control-group"); inError.addClass("error"); @@ -94,7 +102,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location }); - //This tracks if the user is currently saving a new item, we use this to determine + //This tracks if the user is currently saving a new item, we use this to determine // if we should display the warning dialog that they are leaving the page - if a new item // is being saved we never want to display that dialog, this will also cause problems when there // are server side validation issues. @@ -104,6 +112,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.showValidation)) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + notifySubView(); } var unsubscribe = []; @@ -112,6 +121,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function(ev, args) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + notifySubView(); //set the flag so we can check to see if we should display the error. isSavingNewItem = $routeParams.create; })); @@ -121,8 +131,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //remove validation class element.removeClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = false; + notifySubView(); //clear form state as at this point we retrieve new data from the server - //and all validation will have cleared at this point + //and all validation will have cleared at this point formCtrl.$setPristine(); })); @@ -203,7 +214,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location $timeout(function(){ formCtrl.$setPristine(); }, 1000); - + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js index a65a08d17e..097602fe20 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js @@ -10,6 +10,29 @@ function valSubViewDirective() { + function controller($scope, $element) { + //expose api + return { + valStatusChanged: function(args) { + if (!args.form.$valid) { + var subViewContent = $element.find(".ng-invalid"); + + if (subViewContent.length > 0) { + $scope.model.hasError = true; + $scope.model.errorClass = args.showValidation ? 'show-validation' : null; + } else { + $scope.model.hasError = false; + $scope.model.errorClass = null; + } + } + else { + $scope.model.hasError = false; + $scope.model.errorClass = null; + } + } + } + } + function link(scope, el, attr, ctrl) { //if there are no containing form or valFormManager controllers, then we do nothing @@ -43,7 +66,8 @@ var directive = { require: ['?^^form', '?^^valFormManager'], restrict: "A", - link: link + link: link, + controller: controller }; return directive; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html index 3166992e8a..ee46a89490 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html @@ -1,7 +1,8 @@
    - +
  • -
    +
    + ng-class="'sub-view-' + model.name" + val-sub-view> -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html deleted file mode 100644 index 1e72968f46..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html +++ /dev/null @@ -1,21 +0,0 @@ - - - -

    Hours of Umbraco training videos are only a click away

    -

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco, then visit umbraco.tv for even more Umbraco videos.

    - - - - diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemanagement.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemanagement.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemgmt.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemgmt.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/nucache.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/nucache.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/nucache.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/nucache.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/publishedstatus.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/publishedstatus.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/publishedstatus.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/publishedstatus.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.html diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/xmldataintegrityreport.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/xmldataintegrityreport.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/xmldataintegrityreport.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/xmldataintegrityreport.html diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index a46671fb56..961470b03a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -29,7 +29,10 @@ "languages_mandatoryLanguageHelp", "languages_defaultLanguage", "languages_defaultLanguageHelp", - "languages_addLanguage" + "languages_addLanguage", + "languages_noFallbackLanguageOption", + "languages_fallbackLanguageDescription", + "languages_fallbackLanguage" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -39,8 +42,17 @@ vm.labels.defaultLanguage = values[3]; vm.labels.defaultLanguageHelp = values[4]; vm.labels.addLanguage = values[5]; + vm.labels.noFallbackLanguageOption = values[6]; - if($routeParams.create) { + $scope.properties = { + fallbackLanguage: { + alias: "fallbackLanguage", + description: values[7], + label: values[8] + } + }; + + if ($routeParams.create) { vm.page.name = vm.labels.addLanguage; languageResource.getCultures().then(function (culturesDictionary) { var cultures = []; @@ -53,10 +65,17 @@ vm.availableCultures = cultures; }); } - }); - if(!$routeParams.create) { + vm.loading = true; + languageResource.getAll().then(function (languages) { + vm.availableLanguages = languages.filter(function (l) { + return $routeParams.id != l.id; + }); + vm.loading = false; + }); + + if (!$routeParams.create) { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 265fe0fd2b..533337549b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -14,11 +14,11 @@ hide-alias="true"> - + - + @@ -64,6 +64,16 @@ + +
    + +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index 3911d6e893..a5c446dfb5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -13,6 +13,16 @@ vm.editLanguage = editLanguage; vm.deleteLanguage = deleteLanguage; + vm.getLanguageById = function(id) { + for (var i = 0; i < vm.languages.length; i++) { + if (vm.languages[i].id === id) { + return vm.languages[i]; + } + } + + return null; + }; + function init() { vm.loading = true; @@ -21,18 +31,20 @@ var labelKeys = [ "treeHeaders_languages", "general_mandatory", - "general_default" + "general_default", + "languages_fallsbackToLabel" ]; localizationService.localizeMany(labelKeys).then(function (values) { vm.labels.languages = values[0]; vm.labels.mandatory = values[1]; vm.labels.general = values[2]; + vm.labels.fallsbackTo = values[3]; // set page name vm.page.name = vm.labels.languages; }); - languageResource.getAll().then(function(languages) { + languageResource.getAll().then(function (languages) { vm.languages = languages; vm.loading = false; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index 9d3f4db3aa..735bf16d94 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -33,6 +33,7 @@ Language Default Mandatory + Fallback @@ -55,6 +56,10 @@ size="xs"> + + {{vm.getLanguageById(language.fallbackLanguageId).name}} + (none) + + sub-views="vm.page.navigation" model="vm.page">
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index cf23e0691d..48e036f975 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1713,10 +1713,14 @@ To manage your website, simply open the Umbraco back office and start adding con Add language Mandatory language - Properties on this language has to be filled out before the node can be published. + Properties on this language have to be filled out before the node can be published. Default language An Umbraco site can only have one default language set. Switching default language may result in default content missing. + Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language diff --git a/src/Umbraco.Web.UI/config/Dashboard.Release.config b/src/Umbraco.Web.UI/config/Dashboard.Release.config index d4ffc41de8..0a39395213 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.Release.config +++ b/src/Umbraco.Web.UI/config/Dashboard.Release.config @@ -10,6 +10,16 @@ views/dashboard/settings/settingsdashboardintro.html + + + views/dashboard/settings/examinemanagement.html + + + + + views/dashboard/settings/publishedstatus.html + +
    @@ -27,21 +37,6 @@ developer - - - views/dashboard/developer/developerdashboardvideos.html - - - - - views/dashboard/developer/examinemanagement.html - - - - - views/dashboard/developer/publishedstatus.html - -
    @@ -99,11 +94,11 @@
    - developer + settings - views/dashboard/developer/healthcheck.html + views/dashboard/settings/healthcheck.html
    @@ -113,7 +108,7 @@ - views/dashboard/developer/redirecturls.html + views/dashboard/content/redirecturls.html
    diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index afb83ae7f3..c1a1d8815a 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -1,127 +1,122 @@  -
    - - settings - - - - views/dashboard/settings/settingsdashboardintro.html - - -
    -
    - - forms - - - - views/dashboard/forms/formsdashboardintro.html - - -
    -
    - - developer - - - - views/dashboard/developer/developerdashboardvideos.html - - - - - views/dashboard/developer/examinemanagement.html - - - - - views/dashboard/developer/publishedstatus.html - - -
    -
    - - media - - - - views/dashboard/media/mediafolderbrowser.html - - -
    -
    - - forms - - - - views/dashboard/forms/formsdashboardintro.html - - -
    -
    - - translator - - - content - - - - admin - - - views/dashboard/default/startupdashboardintro.html - - -
    -
    - - member - - - - views/dashboard/members/membersdashboardvideos.html - - -
    -
    - - contour - - - plugins/umbracocontour/formsdashboard.ascx - -
    -
    - - developer - - - - views/dashboard/developer/healthcheck.html - - -
    -
    - - content - - - - views/dashboard/developer/redirecturls.html - - -
    -
    - - developer - - - - /App_Plugins/ModelsBuilder/modelsbuilder.htm - - -
    -
    \ No newline at end of file +
    + + settings + + + + views/dashboard/settings/settingsdashboardintro.html + + + + + views/dashboard/settings/examinemanagement.html + + + + + views/dashboard/settings/publishedstatus.html + + +
    +
    + + forms + + + + views/dashboard/forms/formsdashboardintro.html + + +
    +
    + + developer + +
    +
    + + media + + + + views/dashboard/media/mediafolderbrowser.html + + +
    +
    + + forms + + + + views/dashboard/forms/formsdashboardintro.html + + +
    +
    + + translator + + + content + + + + admin + + + views/dashboard/default/startupdashboardintro.html + + +
    +
    + + member + + + + views/dashboard/members/membersdashboardvideos.html + + +
    +
    + + contour + + + plugins/umbracocontour/formsdashboard.ascx + +
    +
    + + settings + + + + views/dashboard/settings/healthcheck.html + + +
    +
    + + content + + + + views/dashboard/content/redirecturls.html + + +
    +
    + + settings + + + + /App_Plugins/ModelsBuilder/modelsbuilder.htm + + +
    + diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index d295cf3d55..3e3f6c75a6 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -249,6 +249,8 @@ namespace Umbraco.Web.Composing public static IPublishedValueFallback PublishedValueFallback => CoreCurrent.PublishedValueFallback; + public static IVariationContextAccessor VariationContextAccessor => CoreCurrent.VariationContextAccessor; + #endregion } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 88abeeb70c..1fcee6d727 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -877,7 +877,7 @@ namespace Umbraco.Web.Editors var canPublish = true; //validate any mandatory variants that are not in the list - var mandatoryLangs = Mapper.Map, IEnumerable>(_allLangs.Value.Values).Where(x => x.Mandatory); + var mandatoryLangs = Mapper.Map, IEnumerable>(_allLangs.Value.Values).Where(x => x.IsMandatory); foreach (var lang in mandatoryLangs) { diff --git a/src/Umbraco.Web/Editors/DashboardHelper.cs b/src/Umbraco.Web/Editors/DashboardHelper.cs index b40f289d04..85f0bc8a28 100644 --- a/src/Umbraco.Web/Editors/DashboardHelper.cs +++ b/src/Umbraco.Web/Editors/DashboardHelper.cs @@ -46,49 +46,52 @@ namespace Umbraco.Web.Editors var tabs = new List>(); var i = 1; - // The dashboard config can contain more than one area inserted by a package. - foreach (var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section))) - { - //we need to validate access to this section - if (DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService) == false) - continue; + //disable packages section dashboard + if (section != "packages") - //User is authorized - foreach (var tab in dashboardSection.Tabs) + // The dashboard config can contain more than one area inserted by a package. + foreach (var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section))) { - //we need to validate access to this tab - if (DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService) == false) + //we need to validate access to this section + if (DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService) == false) continue; - var dashboardControls = new List(); - - foreach (var control in tab.Controls) + //User is authorized + foreach (var tab in dashboardSection.Tabs) { - if (DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService) == false) + //we need to validate access to this tab + if (DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService) == false) continue; - var dashboardControl = new DashboardControl(); - var controlPath = control.ControlPath.Trim(); - dashboardControl.Caption = control.PanelCaption; - dashboardControl.Path = IOHelper.FindFile(controlPath); - if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) - dashboardControl.ServerSide = true; + var dashboardControls = new List(); - dashboardControls.Add(dashboardControl); + foreach (var control in tab.Controls) + { + if (DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService) == false) + continue; + + var dashboardControl = new DashboardControl(); + var controlPath = control.ControlPath.Trim(); + dashboardControl.Caption = control.PanelCaption; + dashboardControl.Path = IOHelper.FindFile(controlPath); + if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) + dashboardControl.ServerSide = true; + + dashboardControls.Add(dashboardControl); + } + + tabs.Add(new Tab + { + Id = i, + Alias = tab.Caption.ToSafeAlias(), + IsActive = i == 1, + Label = tab.Caption, + Properties = dashboardControls + }); + + i++; } - - tabs.Add(new Tab - { - Id = i, - Alias = tab.Caption.ToSafeAlias(), - IsActive = i == 1, - Label = tab.Caption, - Properties = dashboardControls - }); - - i++; } - } //In case there are no tabs or a user doesn't have access the empty tabs list is returned return tabs; diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 8df19027a4..a3e613670b 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -65,15 +65,21 @@ namespace Umbraco.Web.Editors public IHttpActionResult DeleteLanguage(int id) { var language = Services.LocalizationService.GetLanguageById(id); - if (language == null) return NotFound(); + if (language == null) + { + return NotFound(); + } // the service would not let us do it, but test here nevertheless - if (language.IsDefaultVariantLanguage) + if (language.IsDefault) { var message = $"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted."; throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); } + // service is happy deleting a language that's fallback for another language, + // will just remove it - so no need to check here + Services.LocalizationService.Delete(language); return Ok(); @@ -112,31 +118,66 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - //create it - var newLang = new Umbraco.Core.Models.Language(culture.Name) + // create it (creating a new language cannot create a fallback cycle) + var newLang = new Core.Models.Language(culture.Name) { CultureName = culture.DisplayName, - IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, - Mandatory = language.Mandatory + IsDefault = language.IsDefault, + IsMandatory = language.IsMandatory, + FallbackLanguageId = language.FallbackLanguageId }; + Services.LocalizationService.Save(newLang); return Mapper.Map(newLang); } - existing.Mandatory = language.Mandatory; + existing.IsMandatory = language.IsMandatory; // note that the service will prevent the default language from being "un-defaulted" // but does not hurt to test here - though the UI should prevent it too - if (existing.IsDefaultVariantLanguage && !language.IsDefaultVariantLanguage) + if (existing.IsDefault && !language.IsDefault) { - ModelState.AddModelError("IsDefaultVariantLanguage", "Cannot un-default the default language."); + ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - existing.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; + existing.IsDefault = language.IsDefault; + existing.FallbackLanguageId = language.FallbackLanguageId; + + // modifying an existing language can create a fallback, verify + // note that the service will check again, dealing with race conds + if (existing.FallbackLanguageId.HasValue) + { + var languages = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); + if (!languages.ContainsKey(existing.FallbackLanguageId.Value)) + { + ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + if (CreatesCycle(existing, languages)) + { + ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existing.FallbackLanguageId.Value].IsoCode} would create a circular path."); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + } Services.LocalizationService.Save(existing); return Mapper.Map(existing); - } + } + + // see LocalizationService + private bool CreatesCycle(ILanguage language, IDictionary languages) + { + // a new language is not referenced yet, so cannot be part of a cycle + if (!language.HasIdentity) return false; + + var id = language.FallbackLanguageId; + while (true) // assuming languages does not already contains a cycle, this must end + { + if (!id.HasValue) return false; // no fallback means no cycle + if (id.Value == language.Id) return true; // back to language = cycle! + id = languages[id.Value].FallbackLanguageId; // else keep chaining + } + } } } diff --git a/src/Umbraco.Web/Editors/PublishedStatusController.cs b/src/Umbraco.Web/Editors/PublishedStatusController.cs index 087b96a241..937d3f3137 100644 --- a/src/Umbraco.Web/Editors/PublishedStatusController.cs +++ b/src/Umbraco.Web/Editors/PublishedStatusController.cs @@ -18,13 +18,13 @@ namespace Umbraco.Web.Editors public string GetPublishedStatusUrl() { if (_publishedSnapshotService is PublishedCache.XmlPublishedCache.PublishedSnapshotService) - return "views/dashboard/developer/xmldataintegrityreport.html"; + return "views/dashboard/settings/xmldataintegrityreport.html"; //if (service is PublishedCache.PublishedNoCache.PublishedSnapshotService) // return "views/dashboard/developer/nocache.html"; if (_publishedSnapshotService is PublishedCache.NuCache.PublishedSnapshotService) - return "views/dashboard/developer/nucache.html"; + return "views/dashboard/settings/nucache.html"; throw new NotSupportedException("Not supported: " + _publishedSnapshotService.GetType().FullName); } diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs index 6412409488..ac64706292 100644 --- a/src/Umbraco.Web/HealthCheck/HealthCheckController.cs +++ b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.HealthCheck /// /// The API controller used to display the health check info and execute any actions /// - [UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)] + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] public class HealthCheckController : UmbracoAuthorizedJsonController { private readonly HealthCheckCollection _checks; diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index f78d2bd28f..75dd07bf09 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing @@ -20,9 +19,12 @@ namespace Umbraco.Web.Models.ContentEditing public string Name { get; set; } [DataMember(Name = "isDefault")] - public bool IsDefaultVariantLanguage { get; set; } + public bool IsDefault { get; set; } [DataMember(Name = "isMandatory")] - public bool Mandatory { get; set; } + public bool IsMandatory { get; set; } + + [DataMember(Name = "fallbackLanguageId")] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 47e4b3d872..d0218b6639 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,4 +1,8 @@ -using Umbraco.Core.Models.PublishedContent; +using System; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; namespace Umbraco.Web.Models.PublishedContent { @@ -7,86 +11,283 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueFallback : IPublishedValueFallback { - // this is our default implementation - // kinda reproducing what was available in v7 + private readonly ILocalizationService _localizationService; + private readonly IVariationContextAccessor _variationContextAccessor; - /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + /// + /// Initializes a new instance of the class. + /// + public PublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor) { - // no fallback here - return defaultValue; + _localizationService = serviceContext.LocalizationService; + _variationContextAccessor = variationContextAccessor; } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value) { - // no fallback here - return defaultValue; + return TryGetValue(property, culture, segment, fallback, defaultValue, out value); } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value) { - // no fallback here - return defaultValue; + _variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment); + + foreach (var f in fallback) + { + switch (f) + { + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (TryGetValueWithLanguageFallback(property, culture, segment, defaultValue, out value)) + return true; + break; + default: + throw NotSupportedFallbackMethod(f, "property"); + } + } + + value = defaultValue; + return false; } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value) { - // no fallback here - return defaultValue; + return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); } /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) + public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value) { - // no fallback here - if (!recurse) return defaultValue; + var propertyType = content.ContentType.GetPropertyType(alias); + if (propertyType == null) + { + value = default; + return false; + } + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); + foreach (var f in fallback) + { + switch (f) + { + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out value)) + return true; + break; + default: + throw NotSupportedFallbackMethod(f, "element"); + } + } + + value = defaultValue; + return false; + } + + /// + public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value) + { // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, recurse); + return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); } /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) + public virtual bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value) { - // no fallback here - if (!recurse) return defaultValue; + var propertyType = content.ContentType.GetPropertyType(alias); + if (propertyType == null) + { + value = default; + return false; + } + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); - // otherwise, implement recursion as it was implemented in PublishedContentBase + // note: we don't support "recurse & language" which would walk up the tree, + // looking at languages at each level - should someone need it... they'll have + // to implement it. - // fixme caching? - // - // all caches were using PublishedContentBase.GetProperty(alias, recurse) to get the property, - // then, - // NuCache.PublishedContent was storing the property in GetAppropriateCache() with key "NuCache.Property.Recurse[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]"; - // XmlPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedContentCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - // DictionaryPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedMediaCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - // - // at the moment, caching has been entirely removed, until we better understand caching + fallback + foreach (var f in fallback) + { + switch (f) + { + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out value)) + return true; + break; + case Fallback.Ancestors: + if (TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out value)) + return true; + break; + default: + throw NotSupportedFallbackMethod(f, "content"); + } + } + value = defaultValue; + return false; + } + + private NotSupportedException NotSupportedFallbackMethod(int fallback, string level) + { + return new NotSupportedException($"Fallback {GetType().Name} does not support fallback code '{fallback}' at {level} level."); + } + + // tries to get a value, recursing the tree + private static bool TryGetValueWithRecursiveFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + { IPublishedProperty property = null; // if we are here, content's property has no value IPublishedProperty noValueProperty = null; do { content = content.Parent; property = content?.GetProperty(alias); - if (property != null) noValueProperty = property; - } while (content != null && (property == null || property.HasValue(culture, segment) == false)); + if (property != null) + { + noValueProperty = property; + } + } + while (content != null && (property == null || property.HasValue(culture, segment) == false)); // if we found a content with the property having a value, return that property value if (property != null && property.HasValue(culture, segment)) - return property.Value(culture, segment); + { + value = property.Value(culture, segment); + return true; + } // if we found a property, even though with no value, return that property value // because the converter may want to handle the missing value. ie if defaultValue is default, // either specified or by default, the converter may want to substitute something else. if (noValueProperty != null) - return noValueProperty.Value(culture, segment, defaultValue: defaultValue); + { + value = noValueProperty.Value(culture, segment, defaultValue: defaultValue); + return true; + } - // else return default - return defaultValue; + value = defaultValue; + return false; + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (property.HasValue(culture2, segment)) + { + value = property.Value(culture2, segment); + return true; + } + + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(alias, culture2, segment); + return true; + } + + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + // fixme + // _localizationService.GetXxx() is expensive, it deep clones objects + // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(alias, culture2, segment); + return true; + } + + language = language2; + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index f2873e9466..802f42ed9e 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -27,9 +27,16 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => IsMultipleDataType(propertyType.DataType) - ? typeof (IEnumerable) - : typeof (IPublishedContent); + { + var isMultiple = IsMultipleDataType(propertyType.DataType); + var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); + + // hard-coding "image" here but that's how it works at UI level too + + return isMultiple + ? (isOnlyImages ? typeof(IEnumerable<>).MakeGenericType(ModelType.For("image")) : typeof(IEnumerable)) + : (isOnlyImages ? ModelType.For("image") : typeof(IPublishedContent)); + } public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; @@ -40,6 +47,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.Multiple; } + private bool IsOnlyImagesDataType(PublishedDataType dataType) + { + var config = ConfigurationEditor.ConfigurationAs(dataType.Configuration); + return config.OnlyImages; + } + public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 8dd3bb8dc7..132c4b6d59 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -90,7 +90,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // determines whether a property has value public override bool HasValue(string culture = null, string segment = null) { - ContextualizeVariation(ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); var value = GetSourceValue(culture, segment); var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source); @@ -194,7 +194,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override object GetSourceValue(string culture = null, string segment = null) { - ContextualizeVariation(ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); if (culture == "" && segment == "") return _sourceValue; @@ -206,19 +206,9 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - private void ContextualizeVariation(ref string culture, ref string segment) - { - if (culture != null && segment != null) return; - - // use context values - var publishedVariationContext = _content.VariationContextAccessor?.VariationContext; - if (culture == null) culture = _variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; - if (segment == null) segment = _variations.VariesBySegment() ? publishedVariationContext?.Segment : ""; - } - public override object GetValue(string culture = null, string segment = null) { - ContextualizeVariation(ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); object value; lock (_locko) @@ -239,7 +229,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override object GetXPathValue(string culture = null, string segment = null) { - ContextualizeVariation(ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); lock (_locko) { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index dc70e7c348..f0ddf62074 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -186,30 +186,30 @@ namespace Umbraco.Web #region Value /// - /// Recursively the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// 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 variation language. /// The variation segment. - /// A value indicating whether to recurse. + /// Optional fallback strategy. /// The default value. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// 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 IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, Fallback fallback = default, object defaultValue = default) { var property = content.GetProperty(alias); + // if we have a property, and it has a value, return that value if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); + // else let fallback try to get a value + if (PublishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) + return value; + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property?.GetValue(culture, segment); } #endregion @@ -217,35 +217,35 @@ namespace Umbraco.Web #region Value /// - /// Recursively gets the value of a content's property identified by its alias, converted to a specified type. + /// Gets the value of a content's property identified by its alias, converted to a specified type. /// /// The target property type. /// The content. /// The property alias. /// The variation language. /// The variation segment. + /// Optional fallback strategy. /// The default value. - /// A value indicating whether to recurse. /// The value of the content's property identified by the alias, converted to the specified type. - /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// 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, or if it could not be converted, returns default(T). - /// 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 IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, Fallback fallback = default, T defaultValue = default) { var property = content.GetProperty(alias); + // if we have a property, and it has a value, return that value if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); + // else let fallback try to get a value + if (PublishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) + return value; + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(culture, segment); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", int fallback = 0) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedContentPropertyExtension.cs deleted file mode 100644 index fcbfc7f431..0000000000 --- a/src/Umbraco.Web/PublishedContentPropertyExtension.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.Web -{ - /// - /// Provides extension methods for IPublishedProperty. - /// - public static class PublishedPropertyExtension - { - // see notes in PublishedElementExtensions - // - private static IPublishedValueFallback PublishedValueFallback => Current.PublishedValueFallback; - - #region Value - - public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) - { - if (property.HasValue(culture, segment)) - return property.GetValue(culture, segment); - - return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); - } - - #endregion - - #region Value - - public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default) - { - // for Value when defaultValue is not specified, and HasValue() is false, we still want to convert the result (see below) - // but we have no way to tell whether default value is specified or not - we could do it with overloads, but then defaultValue - // comes right after property and conflicts with culture when T is string - so we're just not doing it - if defaultValue is - // default, whether specified or not, we give a chance to the converter - // - //if (!property.HasValue(culture, segment) && 'defaultValue is explicitely specified') return defaultValue; - - // 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(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 - // failed attempt. So, no need to care for value being null here. - - // if already the requested type, return - if (value is T variable) return variable; - - // if can convert to requested type, return - var convert = value.TryConvertTo(); - if (convert.Success) return convert.Result; - - // at that point, the code tried with the raw value - // that makes no sense because it sort of is unpredictable, - // you wouldn't know when the converters run or don't run. - // so, it's commented out now. - - // try with the raw value - //var source = property.ValueSource; - //if (source is string) source = TextValueConverterHelper.ParseStringValueSource((string)source); - //if (source is T) return (T)source; - //convert = source.TryConvertTo(); - //if (convert.Success) return convert.Result; - - return defaultValue; - } - - #endregion - } -} diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index 945270cb9e..cd6ede9a7c 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -98,6 +99,7 @@ namespace Umbraco.Web /// The property alias. /// The variation language. /// The variation segment. + /// Optional fallback strategy. /// The default value. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// @@ -106,14 +108,21 @@ 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 culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, Fallback fallback = default, object defaultValue = default) { var property = content.GetProperty(alias); + // if we have a property, and it has a value, return that value if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + // else let fallback try to get a value + if (PublishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) + return value; + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property?.GetValue(culture, segment); } #endregion @@ -128,6 +137,7 @@ namespace Umbraco.Web /// The property alias. /// The variation language. /// The variation segment. + /// Optional fallback strategy. /// The default value. /// The value of the content's property identified by the alias, converted to the specified type. /// @@ -136,14 +146,21 @@ 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, string culture = null, string segment = null, T defaultValue = default) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, Fallback fallback = default, T defaultValue = default) { var property = content.GetProperty(alias); + // if we have a property, and it has a value, return that value if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + // else let fallback try to get a value + if (PublishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) + return value; + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(culture, segment); } #endregion diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs new file mode 100644 index 0000000000..b431f24828 --- /dev/null +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web +{ + /// + /// Provides extension methods for IPublishedProperty. + /// + public static class PublishedPropertyExtension + { + // see notes in PublishedElementExtensions + // + private static IPublishedValueFallback PublishedValueFallback => Current.PublishedValueFallback; + + #region Value + + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, Fallback fallback = default, object defaultValue = default) + { + if (property.HasValue(culture, segment)) + return property.GetValue(culture, segment); + + return PublishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var value) + ? value + : property.GetValue(culture, segment); // give converter a chance to return it's own vision of "no value" + } + + #endregion + + #region Value + + public static T Value(this IPublishedProperty property, string culture = null, string segment = null, Fallback fallback = default, T defaultValue = default) + { + if (property.HasValue(culture, segment)) + { + // we have a value + // try to cast or convert it + var value = property.GetValue(culture, segment); + if (value is T valueAsT) return valueAsT; + var valueConverted = value.TryConvertTo(); + if (valueConverted) return valueConverted.Result; + + // cannot cast nor convert the value, nothing we can return but 'default' + // note: we don't want to fallback in that case - would make little sense + return default; + } + + // we don't have a value, try fallback + if (PublishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) + return fallbackValue; + + // we don't have a value - neither direct nor fallback + // give a chance to the converter to return something (eg empty enumerable) + var noValue = property.GetValue(culture, segment); + if (noValue is T noValueAsT) return noValueAsT; + var noValueConverted = noValue.TryConvertTo(); + if (noValueConverted) return noValueConverted.Result; + + // cannot cast noValue nor convert it, nothing we can return but 'default' + return default; + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index f37b0fe896..36c3ba5533 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -190,8 +190,16 @@ namespace Umbraco.Web.Routing // this the ONLY place where we deal with default culture - IUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant - if (culture == null) - culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + // (but only for variant content of course) + if (content.ContentType.VariesByCulture()) + { + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + } + else + { + culture = null; + } if (current == null) current = _umbracoContext.CleanedUmbracoUrl; diff --git a/src/Umbraco.Web/Trees/PackagesTreeController.cs b/src/Umbraco.Web/Trees/PackagesTreeController.cs index 31283acebf..235f249509 100644 --- a/src/Umbraco.Web/Trees/PackagesTreeController.cs +++ b/src/Umbraco.Web/Trees/PackagesTreeController.cs @@ -1,107 +1,107 @@ -//using System; -//using System.Linq; -//using System.Net.Http.Formatting; -//using Umbraco.Web.Models.Trees; -//using Umbraco.Web.Mvc; -//using Umbraco.Web.WebApi.Filters; -//using umbraco; -//using umbraco.cms.businesslogic.packager; -//using Umbraco.Core.Services; -//using Umbraco.Web._Legacy.Actions; -//using Constants = Umbraco.Core.Constants; +using System; +using System.Linq; +using System.Net.Http.Formatting; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using umbraco; +using umbraco.cms.businesslogic.packager; +using Umbraco.Core.Services; +using Umbraco.Web._Legacy.Actions; +using Constants = Umbraco.Core.Constants; -//namespace Umbraco.Web.Trees -//{ -// [UmbracoTreeAuthorize(Constants.Trees.Packages)] -// [Tree(Constants.Applications.Developer, Constants.Trees.Packages, null, sortOrder: 0)] -// [PluginController("UmbracoTrees")] -// [CoreTree] -// public class PackagesTreeController : TreeController -// { -// /// -// /// Helper method to create a root model for a tree -// /// -// /// -// protected override TreeNode CreateRootNode(FormDataCollection queryStrings) -// { -// var root = base.CreateRootNode(queryStrings); +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.Packages)] + [Tree(Constants.Applications.Developer, Constants.Trees.Packages, null, sortOrder: 0)] + [PluginController("UmbracoTrees")] + [CoreTree] + public class PackagesTreeController : TreeController + { + /// + /// Helper method to create a root model for a tree + /// + /// + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) + { + var root = base.CreateRootNode(queryStrings); + + root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Developer, Constants.Trees.Packages, "overview"); + + root.Icon = "icon-box"; -// //this will load in a custom UI instead of the dashboard for the root node -// root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Developer, Constants.Trees.Packages, "overview"); -// root.Icon = "icon-box"; + return root; + } + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var nodes = new TreeNodeCollection(); -// return root; -// } -// protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) -// { -// var nodes = new TreeNodeCollection(); + var createdPackages = CreatedPackage.GetAllCreatedPackages(); -// var createdPackages = CreatedPackage.GetAllCreatedPackages(); - -// if (id == "created") -// { -// nodes.AddRange( -// createdPackages -// .OrderBy(entity => entity.Data.Name) -// .Select(dt => -// { -// var node = CreateTreeNode(dt.Data.Id.ToString(), id, queryStrings, dt.Data.Name, "icon-inbox", false, -// string.Format("/{0}/framed/{1}", -// queryStrings.GetValue("application"), -// Uri.EscapeDataString("developer/Packages/EditPackage.aspx?id=" + dt.Data.Id))); -// return node; -// })); -// } -// else -// { -// //must be root -// var node = CreateTreeNode( -// "created", -// id, -// queryStrings, -// Services.TextService.Localize("treeHeaders/createdPackages"), -// "icon-folder", -// createdPackages.Count > 0, -// string.Empty); + if (id == "created") + { + nodes.AddRange( + createdPackages + .OrderBy(entity => entity.Data.Name) + .Select(dt => + { + var node = CreateTreeNode(dt.Data.Id.ToString(), id, queryStrings, dt.Data.Name, "icon-inbox", false, + string.Format("/{0}/framed/{1}", + queryStrings.GetValue("application"), + Uri.EscapeDataString("developer/Packages/EditPackage.aspx?id=" + dt.Data.Id))); + return node; + })); + } + else + { + //must be root + var node = CreateTreeNode( + "created", + id, + queryStrings, + Services.TextService.Localize("treeHeaders/createdPackages"), + "icon-folder", + createdPackages.Count > 0, + string.Empty); -// //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. -// node.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + node.AdditionalData["jsClickCallback"] = "javascript:void(0);"; -// nodes.Add(node); -// } + nodes.Add(node); + } -// return nodes; -// } + return nodes; + } -// protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) -// { -// var menu = new MenuItemCollection(); + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); -// // Root actions -// if (id == "-1") -// { -// menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) -// .ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue("application")); -// } -// else if (id == "created") -// { -// menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) -// .ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue("application")); + // Root actions + if (id == "-1") + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) + .ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue("application")); + } + else if (id == "created") + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) + .ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue("application")); -// menu.Items.Add( -// Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); -// } -// else -// { -// //it's a package node -// menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias)); -// } + menu.Items.Add( + Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + } + else + { + //it's a package node + menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias)); + } -// return menu; -// } -// } -//} + return menu; + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 3e6db493bf..84ec2d0da3 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -906,7 +906,7 @@ - + diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 7caae0dc4b..d15d3d33c1 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -82,7 +82,7 @@ namespace umbraco //check for published content and get its value using that if (publishedContent != null && (publishedContent.HasProperty(_fieldName) || recursive)) { - var pval = publishedContent.Value(_fieldName, recurse: recursive); + var pval = publishedContent.Value(_fieldName, fallback: Fallback.ToAncestors); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } @@ -102,7 +102,7 @@ namespace umbraco { if (publishedContent != null && (publishedContent.HasProperty(altFieldName) || recursive)) { - var pval = publishedContent.Value(altFieldName, recurse: recursive); + var pval = publishedContent.Value(altFieldName, fallback: Fallback.ToAncestors); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; }