Merge remote-tracking branch 'origin/temp8' into temp8-222-variant-invariant-data-updates
# Conflicts: # src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs
This commit is contained in:
@@ -159,6 +159,9 @@ namespace Umbraco.Core.Composing
|
||||
public static IPublishedValueFallback PublishedValueFallback
|
||||
=> _publishedValueFallback ?? Container.GetInstance<IPublishedValueFallback>() ?? new NoopPublishedValueFallback();
|
||||
|
||||
public static IVariationContextAccessor VariationContextAccessor
|
||||
=> Container.GetInstance<IVariationContextAccessor>();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -107,18 +107,35 @@ namespace Umbraco.Core.Migrations.Upgrade
|
||||
|
||||
Chain<UserForeignKeys>("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
|
||||
//Chain<AddTypedLabels>("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove,
|
||||
Chain<AddTypedLabels>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but it after shannon's, with a new target state,
|
||||
Chain<AddTypedLabels>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state,
|
||||
Add<UserForeignKeys>("{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<ContentVariationMigration>("{1350617A-4930-4D61-852F-E3AA9E692173}");
|
||||
Chain<UpdateUmbracoConsent>("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
|
||||
//Chain<FallbackLanguage>("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
|
||||
Chain<AddRelationTypeForMediaFolderOnDelete>("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
|
||||
Chain<IncreaseLanguageIsoCodeColumnLength>("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
|
||||
Chain<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
|
||||
Chain<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
|
||||
Chain<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}");
|
||||
Chain<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
|
||||
//Chain<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
|
||||
|
||||
Chain<FallbackLanguage>("{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<UpdateDefaultMandatoryLanguage>("{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<FallbackLanguage>("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
|
||||
// resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ...
|
||||
|
||||
//Chain<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
|
||||
Chain<RefactorVariantsModel>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
|
||||
From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}")
|
||||
.Chain<FallbackLanguage>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
|
||||
// resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}...
|
||||
|
||||
//FINAL
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for
|
||||
/// a given language.
|
||||
/// </summary>
|
||||
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<LanguageDto>("fallbackLanguageId");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LanguageDto>(u => u
|
||||
.Set(x => x.IsDefaultVariantLanguage, true)
|
||||
.Set(x => x.Mandatory, true))
|
||||
.Set(x => x.IsDefault, true)
|
||||
.Set(x => x.IsMandatory, true))
|
||||
.Where<LanguageDto>(x => x.Id == defaultId);
|
||||
|
||||
Database.Execute(updateDefault);
|
||||
|
||||
@@ -4,34 +4,54 @@ using Umbraco.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a language.
|
||||
/// </summary>
|
||||
public interface ILanguage : IEntity, IRememberBeingDirty
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Iso Code for the Language
|
||||
/// Gets or sets the ISO code of the language.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Culture Name for the Language
|
||||
/// Gets or sets the culture name of the language.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
string CultureName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="CultureInfo"/> object for the current Language
|
||||
/// Gets the <see cref="CultureInfo"/> object for the language.
|
||||
/// </summary>
|
||||
[IgnoreDataMember]
|
||||
CultureInfo CultureInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
bool IsDefaultVariantLanguage { get; set; }
|
||||
[DataMember]
|
||||
bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
bool Mandatory { get; set; }
|
||||
/// <remarks>
|
||||
/// <para>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.</para>
|
||||
/// </remarks>
|
||||
[DataMember]
|
||||
bool IsMandatory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of a fallback language.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>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.</para>
|
||||
/// </remarks>
|
||||
[DataMember]
|
||||
int? FallbackLanguageId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Language, string>(x => x.IsoCode);
|
||||
public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo<Language, string>(x => x.CultureName);
|
||||
public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsDefaultVariantLanguage);
|
||||
public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.Mandatory);
|
||||
public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsDefault);
|
||||
public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsMandatory);
|
||||
public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, int?>(x => x.FallbackLanguageId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Iso Code for the Language
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[DataMember]
|
||||
public string IsoCode
|
||||
{
|
||||
@@ -44,9 +44,7 @@ namespace Umbraco.Core.Models
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _isoCode, Ps.Value.IsoCodeSelector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Culture Name for the Language
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[DataMember]
|
||||
public string CultureName
|
||||
{
|
||||
@@ -54,22 +52,29 @@ namespace Umbraco.Core.Models
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="CultureInfo"/> object for the current Language
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public CultureInfo CultureInfo => CultureInfo.GetCultureInfo(IsoCode);
|
||||
|
||||
public bool IsDefaultVariantLanguage
|
||||
/// <inheritdoc />
|
||||
public bool IsDefault
|
||||
{
|
||||
get => _isDefaultVariantLanguage;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, Ps.Value.IsDefaultVariantLanguageSelector);
|
||||
}
|
||||
|
||||
public bool Mandatory
|
||||
/// <inheritdoc />
|
||||
public bool IsMandatory
|
||||
{
|
||||
get => _mandatory;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? FallbackLanguageId
|
||||
{
|
||||
get => _fallbackLanguageId;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
75
src/Umbraco.Core/Models/PublishedContent/Fallback.cs
Normal file
75
src/Umbraco.Core/Models/PublishedContent/Fallback.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the built-in fallback policies.
|
||||
/// </summary>
|
||||
public struct Fallback : IEnumerable<int>
|
||||
{
|
||||
private readonly int[] _values;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Fallback"/> struct with values.
|
||||
/// </summary>
|
||||
private Fallback(int[] values)
|
||||
{
|
||||
_values = values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ordered set of fallback policies.
|
||||
/// </summary>
|
||||
/// <param name="values"></param>
|
||||
public static Fallback To(params int[] values) => new Fallback(values);
|
||||
|
||||
/// <summary>
|
||||
/// Do not fallback.
|
||||
/// </summary>
|
||||
public const int None = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Fallback to default value.
|
||||
/// </summary>
|
||||
public const int DefaultValue = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fallback to default value policy.
|
||||
/// </summary>
|
||||
public static Fallback ToDefaultValue => new Fallback(new[] { DefaultValue });
|
||||
|
||||
/// <summary>
|
||||
/// Fallback to other languages.
|
||||
/// </summary>
|
||||
public const int Language = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fallback to language policy.
|
||||
/// </summary>
|
||||
public static Fallback ToLanguage => new Fallback(new[] { Language });
|
||||
|
||||
/// <summary>
|
||||
/// Fallback to tree ancestors.
|
||||
/// </summary>
|
||||
public const int Ancestors = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fallback to tree ancestors policy.
|
||||
/// </summary>
|
||||
public static Fallback ToAncestors => new Fallback(new[] { Ancestors });
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<int> GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<int>)_values ?? Array.Empty<int>()).GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,122 @@
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a fallback strategy for getting <see cref="IPublishedElement"/> values.
|
||||
/// </summary>
|
||||
// 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<T>() to trigger fallback
|
||||
/// <summary>
|
||||
/// Tries to get a fallback value for a property.
|
||||
/// </summary>
|
||||
/// <param name="property">The property.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="segment">The requested segment.</param>
|
||||
/// <param name="fallback">A fallback strategy.</param>
|
||||
/// <param name="defaultValue">An optional default value.</param>
|
||||
/// <param name="value">The fallback value.</param>
|
||||
/// <returns>A value indicating whether a fallback value could be provided.</returns>
|
||||
/// <remarks>
|
||||
/// <para>This method is called whenever property.Value(culture, segment, defaultValue) is called, and
|
||||
/// property.HasValue(culture, segment) is false.</para>
|
||||
/// <para>It can only fallback at property level (no recurse).</para>
|
||||
/// <para>At property level, property.GetValue() does *not* implement fallback, and one has to
|
||||
/// get property.Value() or property.Value{T}() to trigger fallback.</para>
|
||||
/// <para>Note that <paramref name="culture"/> and <paramref name="segment"/> may not be contextualized,
|
||||
/// so the variant context should be used to contextualize them (see our default implementation in
|
||||
/// the web project.</para>
|
||||
/// </remarks>
|
||||
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).
|
||||
/// <summary>
|
||||
/// Tries to get a fallback value for a property.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value.</typeparam>
|
||||
/// <param name="property">The property.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="segment">The requested segment.</param>
|
||||
/// <param name="fallback">A fallback strategy.</param>
|
||||
/// <param name="defaultValue">An optional default value.</param>
|
||||
/// <param name="value">The fallback value.</param>
|
||||
/// <returns>A value indicating whether a fallback value could be provided.</returns>
|
||||
/// <remarks>
|
||||
/// <para>This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and
|
||||
/// property.HasValue(culture, segment) is false.</para>
|
||||
/// <para>It can only fallback at property level (no recurse).</para>
|
||||
/// <para>At property level, property.GetValue() does *not* implement fallback, and one has to
|
||||
/// get property.Value() or property.Value{T}() to trigger fallback.</para>
|
||||
/// </remarks>
|
||||
bool TryGetValue<T>(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value);
|
||||
|
||||
object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue);
|
||||
/// <summary>
|
||||
/// Tries to get a fallback value for a published element property.
|
||||
/// </summary>
|
||||
/// <param name="content">The published element.</param>
|
||||
/// <param name="alias">The property alias.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="segment">The requested segment.</param>
|
||||
/// <param name="fallback">A fallback strategy.</param>
|
||||
/// <param name="defaultValue">An optional default value.</param>
|
||||
/// <param name="value">The fallback value.</param>
|
||||
/// <returns>A value indicating whether a fallback value could be provided.</returns>
|
||||
/// <remarks>
|
||||
/// <para>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.</para>
|
||||
/// <para>It can only fallback at element level (no recurse).</para>
|
||||
/// </remarks>
|
||||
bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value);
|
||||
|
||||
// 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).
|
||||
/// <summary>
|
||||
/// Tries to get a fallback value for a published element property.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value.</typeparam>
|
||||
/// <param name="content">The published element.</param>
|
||||
/// <param name="alias">The property alias.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="segment">The requested segment.</param>
|
||||
/// <param name="fallback">A fallback strategy.</param>
|
||||
/// <param name="defaultValue">An optional default value.</param>
|
||||
/// <param name="value">The fallback value.</param>
|
||||
/// <returns>A value indicating whether a fallback value could be provided.</returns>
|
||||
/// <remarks>
|
||||
/// <para>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.</para>
|
||||
/// <para>It can only fallback at element level (no recurse).</para>
|
||||
/// </remarks>
|
||||
bool TryGetValue<T>(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
|
||||
|
||||
T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue);
|
||||
/// <summary>
|
||||
/// Tries to get a fallback value for a published content property.
|
||||
/// </summary>
|
||||
/// <param name="content">The published element.</param>
|
||||
/// <param name="alias">The property alias.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="segment">The requested segment.</param>
|
||||
/// <param name="fallback">A fallback strategy.</param>
|
||||
/// <param name="defaultValue">An optional default value.</param>
|
||||
/// <param name="value">The fallback value.</param>
|
||||
/// <returns>A value indicating whether a fallback value could be provided.</returns>
|
||||
/// <remarks>
|
||||
/// <para>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.</para>
|
||||
/// </remarks>
|
||||
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<T>(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<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse);
|
||||
/// <summary>
|
||||
/// Tries to get a fallback value for a published content property.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value.</typeparam>
|
||||
/// <param name="content">The published element.</param>
|
||||
/// <param name="alias">The property alias.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="segment">The requested segment.</param>
|
||||
/// <param name="fallback">A fallback strategy.</param>
|
||||
/// <param name="defaultValue">An optional default value.</param>
|
||||
/// <param name="value">The fallback value.</param>
|
||||
/// <returns>A value indicating whether a fallback value could be provided.</returns>
|
||||
/// <remarks>
|
||||
/// <para>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.</para>
|
||||
/// </remarks>
|
||||
bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,17 +49,24 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// <param name="modelTypes">The model types map.</param>
|
||||
/// <returns>The actual Clr type.</returns>
|
||||
public static Type Map(Type type, Dictionary<string, Type> modelTypes)
|
||||
=> Map(type, modelTypes, false);
|
||||
|
||||
public static Type Map(Type type, Dictionary<string, Type> modelTypes, bool dictionaryIsInvariant)
|
||||
{
|
||||
// it may be that senders forgot to send an invariant dictionary (garbage-in)
|
||||
if (!dictionaryIsInvariant)
|
||||
modelTypes = new Dictionary<string, Type>(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
|
||||
/// <param name="map">The model types map.</param>
|
||||
/// <returns>The actual Clr type name.</returns>
|
||||
public static string MapToName(Type type, Dictionary<string, string> map)
|
||||
=> MapToName(type, map, false);
|
||||
|
||||
private static string MapToName(Type type, Dictionary<string, string> map, bool dictionaryIsInvariant)
|
||||
{
|
||||
// it may be that senders forgot to send an invariant dictionary (garbage-in)
|
||||
if (!dictionaryIsInvariant)
|
||||
map = new Dictionary<string, string>(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) + ">";
|
||||
}
|
||||
|
||||
@@ -9,21 +9,45 @@
|
||||
public class NoopPublishedValueFallback : IPublishedValueFallback
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue;
|
||||
public bool TryGetValue<T>(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue;
|
||||
public bool TryGetValue<T>(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue;
|
||||
public bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,33 +10,51 @@ namespace Umbraco.Core.Persistence.Dtos
|
||||
{
|
||||
public const string TableName = Constants.DatabaseSchema.Tables.Language;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the language.
|
||||
/// </summary>
|
||||
[Column("id")]
|
||||
[PrimaryKeyColumn(IdentitySeed = 2)]
|
||||
public short Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ISO code of the language.
|
||||
/// </summary>
|
||||
[Column("languageISOCode")]
|
||||
[Index(IndexTypes.UniqueNonClustered)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
[Length(14)]
|
||||
public string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the culture name of the language.
|
||||
/// </summary>
|
||||
[Column("languageCultureName")]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
[Length(100)]
|
||||
public string CultureName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Column("isDefaultVariantLang")]
|
||||
[Constraint(Default = "0")]
|
||||
public bool IsDefaultVariantLanguage { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Column("mandatory")]
|
||||
[Constraint(Default = "0")]
|
||||
public bool Mandatory { get; set; }
|
||||
public bool IsMandatory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of a fallback language.
|
||||
/// </summary>
|
||||
[Column("fallbackLanguageId")]
|
||||
[ForeignKey(typeof(LanguageDto), Column = "id")]
|
||||
[Index(IndexTypes.NonClustered)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public int? FallbackLanguageId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
var sqlClause = GetBaseQuery(false);
|
||||
var translator = new SqlTranslator<ILanguage>(sqlClause, query);
|
||||
var sql = translator.Translate();
|
||||
return Database.Fetch<LanguageDto>(sql).Select(ConvertFromDto);
|
||||
var dtos = Database.Fetch<LanguageDto>(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<LanguageDto>(u => u.Set(x => x.IsDefaultVariantLanguage, false));
|
||||
.Update<LanguageDto>(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<LanguageDto>(u => u.Set(x => x.IsDefaultVariantLanguage, false));
|
||||
.Update<LanguageDto>(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<LanguageDto>(x => x.Id)
|
||||
.From<LanguageDto>()
|
||||
.Where<LanguageDto>(x => x.IsDefaultVariantLanguage);
|
||||
.Where<LanguageDto>(x => x.IsDefault);
|
||||
|
||||
var defaultId = Database.ExecuteScalar<int>(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<LanguageDto>(x => x.Id)
|
||||
.From<LanguageDto>()
|
||||
.Where<LanguageDto>(x => x.IsDefaultVariantLanguage);
|
||||
.Where<LanguageDto>(x => x.IsDefault);
|
||||
|
||||
var defaultId = Database.ExecuteScalar<int>(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<LanguageDto>(u => u
|
||||
.Set(x => x.FallbackLanguageId, null))
|
||||
.Where<LanguageDto>(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)
|
||||
{
|
||||
// ensure cache is populated, in a non-expensive way
|
||||
@@ -287,7 +297,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
var languages = (TypedCachePolicy?.GetAllCached(PerformGetAll) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests)
|
||||
?? CachePolicy.GetAll(Array.Empty<int>(), 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ILanguage>(language);
|
||||
if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs))
|
||||
{
|
||||
@@ -380,6 +390,20 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
private bool CreatesCycle(ILanguage language, IDictionary<int, ILanguage> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a <see cref="ILanguage"/> by removing it (but not its usages) from the db
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorVariantsModel.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\SuperZero.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\TagsMigration.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\FallbackLanguage.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\UpdateDefaultMandatoryLanguage.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\UserForeignKeys.cs" />
|
||||
<Compile Include="Models\AuditEntry.cs" />
|
||||
@@ -391,12 +392,14 @@
|
||||
<Compile Include="Models\PathValidationExtensions.cs" />
|
||||
<Compile Include="Models\Entities\TreeEntityBase.cs" />
|
||||
<Compile Include="Models\PropertyTagsExtensions.cs" />
|
||||
<Compile Include="Models\PublishedContent\Fallback.cs" />
|
||||
<Compile Include="Models\PublishedContent\NoopPublishedValueFallback.cs" />
|
||||
<Compile Include="Models\PublishedContent\PublishedCultureInfos.cs" />
|
||||
<Compile Include="Models\PublishedContent\IVariationContextAccessor.cs" />
|
||||
<Compile Include="Models\PublishedContent\IPublishedValueFallback.cs" />
|
||||
<Compile Include="Models\PublishedContent\VariationContext.cs" />
|
||||
<Compile Include="Models\PublishedContent\ThreadCultureVariationContextAccessor.cs" />
|
||||
<Compile Include="Models\PublishedContent\VariationContextAccessorExtensions.cs" />
|
||||
<Compile Include="Persistence\Dtos\AuditEntryDto.cs" />
|
||||
<Compile Include="Persistence\Dtos\ConsentDto.cs" />
|
||||
<Compile Include="Persistence\Dtos\ContentVersionCultureVariationDto.cs" />
|
||||
|
||||
Reference in New Issue
Block a user