Merge branch 'temp8' into temp8-223-scheduled-publishing

This commit is contained in:
Mads Rasmussen
2018-10-08 09:42:04 +02:00
77 changed files with 2116 additions and 913 deletions
+3
View File
@@ -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);
+27 -7
View File
@@ -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; }
}
}
+18 -13
View File
@@ -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);
}
}
}
@@ -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)
{
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
@@ -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;
+3
View File
@@ -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" />
@@ -90,7 +90,7 @@ namespace Umbraco.Tests.Composing
Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree]
typesFound = TypeFinder.FindClassesWithAttribute<TreeAttribute>(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()
@@ -1,114 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<dashBoard>
<section alias="StartupSettingsDashboardSection">
<areas>
<area>settings</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="hello">
views/dashboard/settings/settingsdashboardintro.html
</control>
<control showOnce="false" addPanel="false" panelCaption="">
views/dashboard/settings/settingsdashboardvideos.html
</control>
</tab>
</section>
<section alias="StartupSettingsDashboardSection">
<areas>
<area>settings</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="hello">
views/dashboard/settings/settingsdashboardintro.html
</control>
<control showOnce="false" addPanel="false" panelCaption="">
views/dashboard/settings/settingsdashboardvideos.html
</control>
</tab>
<tab caption="Examine Management">
<control>dashboard/ExamineManagement.ascx</control>
</tab>
</section>
<section alias="StartupDeveloperDashboardSection">
<areas>
<area>developer</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/developer/developerdashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/developer/developerdashboardvideos.html
</control>
</tab>
<tab caption="Examine Management">
<control>dashboard/ExamineManagement.ascx</control>
</tab>
</section>
<section alias="StartupDeveloperDashboardSection">
<areas>
<area>developer</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/developer/developerdashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/developer/developerdashboardvideos.html
</control>
</tab>
</section>
<section alias="StartupMediaDashboardSection">
<areas>
<area>media</area>
</areas>
<tab caption="Content">
<control showOnce="false" addPanel="false" panelCaption="">
views/dashboard/media/mediafolderbrowser.html
</control>
</tab>
<tab caption="Get Started">
<access>
<grant>admin</grant>
</access>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/media/mediadashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/media/desktopmediauploader.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/media/mediadashboardvideos.html
</control>
</tab>
</section>
<section alias="StartupMediaDashboardSection">
<areas>
<area>media</area>
</areas>
<tab caption="Content">
<control showOnce="false" addPanel="false" panelCaption="">
views/dashboard/media/mediafolderbrowser.html
</control>
</tab>
<tab caption="Get Started">
<access>
<grant>admin</grant>
</access>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/media/mediadashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/media/desktopmediauploader.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/media/mediadashboardvideos.html
</control>
</tab>
</section>
<section alias="StartupDashboardSection">
<access>
<deny>translator</deny>
<grant>hello</grant>
<grantBySection>world</grantBySection>
</access>
<areas>
<area>content</area>
</areas>
<tab caption="Get Started">
<access>
<grant>admin</grant>
</access>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardkits.html
<section alias="StartupDashboardSection">
<access>
<deny>editor</deny>
<deny>writer</deny>
<deny>translator</deny>
<grant>hello</grant>
<grantBySection>world</grantBySection>
</access>
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardvideos.html
</control>
</tab>
<tab caption="Last Edits">
<control addPanel="true" MaxRecords="30">dashboard/latestEdits.ascx</control>
</tab>
<tab caption="Change Password">
<control addPanel="true">
views/dashboard/changepassword.html
</control>
</tab>
</section>
<areas>
<area>content</area>
</areas>
<tab caption="Get Started">
<access>
<grant>admin</grant>
</access>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardkits.html
<access>
<deny>editor</deny>
<deny>writer</deny>
</access>
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardvideos.html
</control>
</tab>
<tab caption="Last Edits">
<control addPanel="true" MaxRecords="30">dashboard/latestEdits.ascx</control>
</tab>
<tab caption="Change Password">
<control addPanel="true">
views/dashboard/changepassword.html
</control>
</tab>
</section>
<section alias="StartupMemberDashboardSection">
<areas>
<area>default</area>
<area>member</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/members/membersdashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
members/membersearch.ascx
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/members/membersdashboardvideos.html
</control>
</tab>
</section>
<section alias="StartupMemberDashboardSection">
<areas>
<area>default</area>
<area>member</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/members/membersdashboardintro.html
</control>
<control showOnce="true" addPanel="true" panelCaption="">
members/membersearch.ascx
</control>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/members/membersdashboardvideos.html
</control>
</tab>
</section>
</dashBoard>
@@ -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]
@@ -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
@@ -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<Language>
{
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<int>()))
.Returns((int id) => languages.SingleOrDefault(y => y.Id == id));
localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny<string>()))
.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<string>(), 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<IPublishedProperty>
{
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<IPublishedProperty>
{
prop3
}
};
item1.Children = new List<IPublishedContent> { 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<NotSupportedException>(() => 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);
}
}
}
@@ -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<string>(), props);
var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty<string>(), props);
var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty<string>(), 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<IPublishedProperty>
{
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<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
Container.RegisterSingleton<IPublishedModelFactory>(f => new PublishedModelFactory(f.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
}
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<IPublishedSnapshotService>();
publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny<string>())).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<IUrlProvider>(),
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<IPublishedProperty>
{
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<ILogger>())) { Id = 1 });
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
var caches = new SolidPublishedSnapshot();
var cache = caches.InnerContentCache;
var props = new[]
{
factory.CreatePropertyType("prop1", 1),
};
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), props);
var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty<string>(), props);
var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty<string>(), 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<IPublishedProperty>
{
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<IPublishedProperty>
{
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<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
return caches;
}
}
}
@@ -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<IPublishedModelFactory>(f => new PublishedModelFactory(f.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
}
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<IPublishedSnapshotService>();
publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny<string>())).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<IUrlProvider>(),
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<ILogger>())) { Id = 1 });
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
var caches = new SolidPublishedSnapshot();
var cache = caches.InnerContentCache;
PopulateCache(factory, cache);
return caches;
}
internal abstract void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache);
}
}
@@ -38,7 +38,7 @@ namespace Umbraco.Tests.PublishedContent
var logger = Mock.Of<ILogger>();
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);
}
@@ -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<string, object> _solidSourceValues = new Dictionary<string, object>();
private readonly IDictionary<string, object> _solidValues = new Dictionary<string, object>();
private readonly IDictionary<string, object> _solidXPathValues = new Dictionary<string, object>();
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")]
@@ -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");
@@ -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]
+2
View File
@@ -123,6 +123,8 @@
<Compile Include="Migrations\MigrationTests.cs" />
<Compile Include="Models\PathValidationTests.cs" />
<Compile Include="Models\VariationTests.cs" />
<Compile Include="PublishedContent\PublishedContentLanguageVariantTests.cs" />
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
<Compile Include="PublishedContent\NuCacheTests.cs" />
<Compile Include="Testing\Objects\TestDataSource.cs" />
+7 -7
View File
@@ -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",
@@ -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.
@@ -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) {
@@ -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);
}
};
}
@@ -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;
@@ -1,7 +1,8 @@
<ul class="umb-sub-views-nav" ng-show="showNavigation">
<li ng-repeat="item in navigation | limitTo: itemsLimit ">
<div ng-show="item.alias !== 'more'">
<div ng-show="item.alias !== 'more'"
ng-class="item.errorClass">
<a data-element="sub-view-{{item.alias}}"
tabindex="-1"
class="umb-sub-views-nav-item js-umb-sub-views-nav-item"
@@ -1,10 +1,11 @@
<div class="umb-editor-sub-view"
ng-class="'sub-view-' + model.name"
val-sub-view>
ng-class="'sub-view-' + model.name"
val-sub-view>
<div class="umb-editor-sub-view__content"
ng-show="model.active === true"
ng-include="model.view">
<div
class="umb-editor-sub-view__content"
ng-show="model.active === true"
ng-include="model.view">
</div>
</div>
@@ -1,21 +0,0 @@
<umb-box>
<umb-box-content>
<h3 class="bold">Hours of Umbraco training videos are only a click away</h3>
<p>Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco, then visit <a class="btn-link -underline" href="https://umbraco.tv" target="_blank">umbraco.tv</a> for even more Umbraco videos.</p>
</umb-box-content>
</umb-box>
<div ng-init="init('https://umbraco.tv/videos/developer/chapterrss?sort=no')"
ng-controller="Umbraco.Dashboard.StartupVideosController">
<div class="umb-getstartedcards">
<a class="umb-getstartedcard" ng-repeat="video in videos" href="{{video.link}}" title="{{video.title}}" target="_blank" rel="noopener">
<img ng-src="{{video.thumbnail}}" alt="{{video.title}}">
<div class="umb-getstartedbody">
<p>{{video.title}}</p>
</div>
</a>
</div>
</div>
@@ -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;
@@ -14,11 +14,11 @@
hide-alias="true">
</umb-editor-header>
<umb-editor-container>
<umb-editor-container class="form-horizontal">
<umb-load-indicator ng-if="vm.loading"></umb-load-indicator>
<umb-box ng-if="!vm.loading" class="block-form">
<umb-box ng-if="!vm.loading">
<umb-box-content>
<umb-control-group label="@general_language" ng-if="vm.availableCultures">
@@ -64,6 +64,16 @@
</umb-control-group>
<umb-property property="properties.fallbackLanguage">
<div>
<select name="fallbackLanguage"
ng-model="vm.language.fallbackLanguageId"
ng-options="l.id as l.name for l in vm.availableLanguages">
<option value="">{{vm.labels.noFallbackLanguageOption}}</option>
</select>
</div>
</umb-property>
</umb-box-content>
</umb-box>
@@ -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;
});
@@ -33,6 +33,7 @@
<th>Language</th>
<th>Default</th>
<th>Mandatory</th>
<th>Fallback</th>
</tr>
</thead>
<tbody>
@@ -55,6 +56,10 @@
size="xs">
</umb-checkmark>
</td>
<td>
<span ng-if="language.fallbackLanguageId">{{vm.getLanguageById(language.fallbackLanguageId).name}}</span>
<span ng-if="!language.fallbackLanguageId">(none)</span>
</td>
<td style="text-align: right;">
<umb-button
ng-if="!language.isDefault"
@@ -27,25 +27,28 @@
{
"name": "Packages",
"icon": "icon-cloud",
"view": "views/packager/views/repo.html",
"active": !installPackageUri || installPackageUri === "navigation"
"view": "views/packages/views/repo.html",
"active": !installPackageUri || installPackageUri === "navigation",
"alias": "umbPackages"
},
{
"name": "Installed",
"icon": "icon-box",
"view": "views/packager/views/installed.html",
"active": installPackageUri === "installed"
"view": "views/packages/views/installed.html",
"active": installPackageUri === "installed",
"alias": "umbInstalled"
},
{
"name": "Install local",
"icon": "icon-add",
"view": "views/packager/views/install-local.html",
"active": installPackageUri === "local"
"view": "views/packages/views/install-local.html",
"active": installPackageUri === "local",
"alias": "umbInstallLocal"
}
];
$timeout(function () {
navigationService.syncTree({ tree: "packager", path: "-1" });
navigationService.syncTree({ tree: "packages", path: "-1" });
});
}
@@ -16,7 +16,7 @@
<umb-editor-container>
<umb-editor-sub-views
sub-views="vm.page.navigation">
sub-views="vm.page.navigation" model="vm.page">
</umb-editor-sub-views>
</umb-editor-container>
@@ -1713,10 +1713,14 @@ To manage your website, simply open the Umbraco back office and start adding con
<area alias="languages">
<key alias="addLanguage">Add language</key>
<key alias="mandatoryLanguage">Mandatory language</key>
<key alias="mandatoryLanguageHelp">Properties on this language has to be filled out before the node can be published.</key>
<key alias="mandatoryLanguageHelp">Properties on this language have to be filled out before the node can be published.</key>
<key alias="defaultLanguage">Default language</key>
<key alias="defaultLanguageHelp">An Umbraco site can only have one default language set.</key>
<key alias="changingDefaultLanguageWarning">Switching default language may result in default content missing.</key>
<key alias="fallsbackToLabel">Falls back to</key>
<key alias="noFallbackLanguageOption">No fall back language</key>
<key alias="fallbackLanguageDescription">To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.</key>
<key alias="fallbackLanguage">Fall back language</key>
</area>
<area alias="modelsBuilder">
@@ -10,6 +10,16 @@
views/dashboard/settings/settingsdashboardintro.html
</control>
</tab>
<tab caption="Examine Management">
<control>
views/dashboard/settings/examinemanagement.html
</control>
</tab>
<tab caption="Published Status">
<control>
views/dashboard/settings/publishedstatus.html
</control>
</tab>
</section>
<section alias="StartupFormsDashboardSection">
@@ -27,21 +37,6 @@
<areas>
<area>developer</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/developer/developerdashboardvideos.html
</control>
</tab>
<tab caption="Examine Management">
<control>
views/dashboard/developer/examinemanagement.html
</control>
</tab>
<tab caption="Published Status">
<control>
views/dashboard/developer/publishedstatus.html
</control>
</tab>
</section>
<section alias="StartupMediaDashboardSection">
@@ -99,11 +94,11 @@
<section alias="UmbracoHealthCheck">
<areas>
<area>developer</area>
<area>settings</area>
</areas>
<tab caption="Health Check">
<control>
views/dashboard/developer/healthcheck.html
views/dashboard/settings/healthcheck.html
</control>
</tab>
</section>
@@ -113,7 +108,7 @@
</areas>
<tab caption="Redirect URL Management">
<control>
views/dashboard/developer/redirecturls.html
views/dashboard/content/redirecturls.html
</control>
</tab>
</section>
+120 -125
View File
@@ -1,127 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<dashBoard>
<section alias="StartupSettingsDashboardSection">
<areas>
<area>settings</area>
</areas>
<tab caption="Welcome">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/settings/settingsdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupFormsDashboardSection">
<areas>
<area>forms</area>
</areas>
<tab caption="Install Umbraco Forms">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/forms/formsdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupDeveloperDashboardSection">
<areas>
<area>developer</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/developer/developerdashboardvideos.html
</control>
</tab>
<tab caption="Examine Management">
<control>
views/dashboard/developer/examinemanagement.html
</control>
</tab>
<tab caption="Published Status">
<control>
views/dashboard/developer/publishedstatus.html
</control>
</tab>
</section>
<section alias="StartupMediaDashboardSection">
<areas>
<area>media</area>
</areas>
<tab caption="Content">
<control showOnce="false" addPanel="false" panelCaption="">
views/dashboard/media/mediafolderbrowser.html
</control>
</tab>
</section>
<section alias="StartupFormsDashboardSection">
<areas>
<area>forms</area>
</areas>
<tab caption="Install Umbraco Forms">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/forms/formsdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupDashboardSection">
<access>
<deny>translator</deny>
</access>
<areas>
<area>content</area>
</areas>
<tab caption="Get Started">
<access>
<grant>admin</grant>
</access>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupMemberDashboardSection">
<areas>
<area>member</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/members/membersdashboardvideos.html
</control>
</tab>
</section>
<section alias="contour">
<areas>
<area>contour</area>
</areas>
<tab caption="Dashboard">
<control>plugins/umbracocontour/formsdashboard.ascx</control>
</tab>
</section>
<section alias="UmbracoHealthCheck">
<areas>
<area>developer</area>
</areas>
<tab caption="Health Check">
<control>
views/dashboard/developer/healthcheck.html
</control>
</tab>
</section>
<section alias="RedirectUrlManagement">
<areas>
<area>content</area>
</areas>
<tab caption="Redirect URL Management">
<control>
views/dashboard/developer/redirecturls.html
</control>
</tab>
</section>
<section alias="UmbracoModelsBuilder">
<areas>
<area>developer</area>
</areas>
<tab caption="Models Builder">
<control>
/App_Plugins/ModelsBuilder/modelsbuilder.htm
</control>
</tab>
</section>
</dashBoard>
<section alias="StartupSettingsDashboardSection">
<areas>
<area>settings</area>
</areas>
<tab caption="Welcome">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/settings/settingsdashboardintro.html
</control>
</tab>
<tab caption="Examine Management">
<control>
views/dashboard/settings/examinemanagement.html
</control>
</tab>
<tab caption="Published Status">
<control>
views/dashboard/settings/publishedstatus.html
</control>
</tab>
</section>
<section alias="StartupFormsDashboardSection">
<areas>
<area>forms</area>
</areas>
<tab caption="Install Umbraco Forms">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/forms/formsdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupDeveloperDashboardSection">
<areas>
<area>developer</area>
</areas>
</section>
<section alias="StartupMediaDashboardSection">
<areas>
<area>media</area>
</areas>
<tab caption="Content">
<control showOnce="false" addPanel="false" panelCaption="">
views/dashboard/media/mediafolderbrowser.html
</control>
</tab>
</section>
<section alias="StartupFormsDashboardSection">
<areas>
<area>forms</area>
</areas>
<tab caption="Install Umbraco Forms">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/forms/formsdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupDashboardSection">
<access>
<deny>translator</deny>
</access>
<areas>
<area>content</area>
</areas>
<tab caption="Get Started">
<access>
<grant>admin</grant>
</access>
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/default/startupdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupMemberDashboardSection">
<areas>
<area>member</area>
</areas>
<tab caption="Get Started">
<control showOnce="true" addPanel="true" panelCaption="">
views/dashboard/members/membersdashboardvideos.html
</control>
</tab>
</section>
<section alias="contour">
<areas>
<area>contour</area>
</areas>
<tab caption="Dashboard">
<control>plugins/umbracocontour/formsdashboard.ascx</control>
</tab>
</section>
<section alias="UmbracoHealthCheck">
<areas>
<area>settings</area>
</areas>
<tab caption="Health Check">
<control>
views/dashboard/settings/healthcheck.html
</control>
</tab>
</section>
<section alias="RedirectUrlManagement">
<areas>
<area>content</area>
</areas>
<tab caption="Redirect URL Management">
<control>
views/dashboard/content/redirecturls.html
</control>
</tab>
</section>
<section alias="UmbracoModelsBuilder">
<areas>
<area>settings</area>
</areas>
<tab caption="Models Builder">
<control>
/App_Plugins/ModelsBuilder/modelsbuilder.htm
</control>
</tab>
</section>
</dashBoard>
+2
View File
@@ -249,6 +249,8 @@ namespace Umbraco.Web.Composing
public static IPublishedValueFallback PublishedValueFallback => CoreCurrent.PublishedValueFallback;
public static IVariationContextAccessor VariationContextAccessor => CoreCurrent.VariationContextAccessor;
#endregion
}
}
+1 -1
View File
@@ -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<ILanguage>, IEnumerable<Language>>(_allLangs.Value.Values).Where(x => x.Mandatory);
var mandatoryLangs = Mapper.Map<IEnumerable<ILanguage>, IEnumerable<Language>>(_allLangs.Value.Values).Where(x => x.IsMandatory);
foreach (var lang in mandatoryLangs)
{
+36 -33
View File
@@ -46,49 +46,52 @@ namespace Umbraco.Web.Editors
var tabs = new List<Tab<DashboardControl>>();
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<DashboardControl>();
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<DashboardControl>();
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<DashboardControl>
{
Id = i,
Alias = tab.Caption.ToSafeAlias(),
IsActive = i == 1,
Label = tab.Caption,
Properties = dashboardControls
});
i++;
}
tabs.Add(new Tab<DashboardControl>
{
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;
+52 -11
View File
@@ -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<Language>(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<Language>(existing);
}
}
// see LocalizationService
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
}
}
}
}
@@ -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);
}
@@ -15,7 +15,7 @@ namespace Umbraco.Web.HealthCheck
/// <summary>
/// The API controller used to display the health check info and execute any actions
/// </summary>
[UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)]
[UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)]
public class HealthCheckController : UmbracoAuthorizedJsonController
{
private readonly HealthCheckCollection _checks;
@@ -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; }
}
}
@@ -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
/// </summary>
public class PublishedValueFallback : IPublishedValueFallback
{
// this is our default implementation
// kinda reproducing what was available in v7
private readonly ILocalizationService _localizationService;
private readonly IVariationContextAccessor _variationContextAccessor;
/// <inheritdoc />
public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue)
/// <summary>
/// Initializes a new instance of the <see cref="PublishedValueFallback"/> class.
/// </summary>
public PublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor)
{
// no fallback here
return defaultValue;
_localizationService = serviceContext.LocalizationService;
_variationContextAccessor = variationContextAccessor;
}
/// <inheritdoc />
public T GetValue<T>(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<object>(property, culture, segment, fallback, defaultValue, out value);
}
/// <inheritdoc />
public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue)
public bool TryGetValue<T>(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;
}
/// <inheritdoc />
public T GetValue<T>(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<object>(content, alias, culture, segment, fallback, defaultValue, out value);
}
/// <inheritdoc />
public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse)
public bool TryGetValue<T>(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;
}
/// <inheritdoc />
public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
// is that ok?
return GetValue<object>(content, alias, culture, segment, defaultValue, recurse);
return TryGetValue<object>(content, alias, culture, segment, fallback, defaultValue, out value);
}
/// <inheritdoc />
public T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse)
public virtual bool TryGetValue<T>(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<T>(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<T>(culture, segment);
{
value = property.Value<T>(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<T>(culture, segment, defaultValue: defaultValue);
{
value = noValueProperty.Value<T>(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<T>(IPublishedProperty property, string culture, string segment, T defaultValue, out T value)
{
value = defaultValue;
if (culture.IsNullOrWhiteSpace()) return false;
var visited = new HashSet<int>();
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<T>(culture2, segment);
return true;
}
language = language2;
}
}
// tries to get a value, falling back onto other languages
private bool TryGetValueWithLanguageFallback<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value)
{
value = defaultValue;
if (culture.IsNullOrWhiteSpace()) return false;
var visited = new HashSet<int>();
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<T>(alias, culture2, segment);
return true;
}
language = language2;
}
}
// tries to get a value, falling back onto other languages
private bool TryGetValueWithLanguageFallback<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value)
{
value = defaultValue;
if (culture.IsNullOrWhiteSpace()) return false;
var visited = new HashSet<int>();
// 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<T>(alias, culture2, segment);
return true;
}
language = language2;
}
}
}
}
@@ -27,9 +27,16 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
}
public override Type GetPropertyValueType(PublishedPropertyType propertyType)
=> IsMultipleDataType(propertyType.DataType)
? typeof (IEnumerable<IPublishedContent>)
: 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<IPublishedContent>))
: (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<MediaPickerConfiguration>(dataType.Configuration);
return config.OnlyImages;
}
public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
{
if (source == null) return null;
@@ -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)
{
+23 -23
View File
@@ -186,30 +186,30 @@ namespace Umbraco.Web
#region Value
/// <summary>
/// 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.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The variation language.</param>
/// <param name="segment">The variation segment.</param>
/// <param name="recurse">A value indicating whether to recurse.</param>
/// <param name="fallback">Optional fallback strategy.</param>
/// <param name="defaultValue">The default value.</param>
/// <returns>The value of the content's property identified by the alias, if it exists, otherwise a default value.</returns>
/// <remarks>
/// <para>Recursively means: walking up the tree from <paramref name="content"/>, get the first value that can be found.</para>
/// <para>The value comes from <c>IPublishedProperty</c> field <c>Value</c> ie it is suitable for use when rendering content.</para>
/// <para>If no property with the specified alias exists, or if the property has no value, returns <paramref name="defaultValue"/>.</para>
/// <para>If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter.</para>
/// <para>The alias is case-insensitive.</para>
/// </remarks>
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<T>
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
/// <param name="content">The content.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The variation language.</param>
/// <param name="segment">The variation segment.</param>
/// <param name="fallback">Optional fallback strategy.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="recurse">A value indicating whether to recurse.</param>
/// <returns>The value of the content's property identified by the alias, converted to the specified type.</returns>
/// <remarks>
/// <para>Recursively means: walking up the tree from <paramref name="content"/>, get the first value that can be found.</para>
/// <para>The value comes from <c>IPublishedProperty</c> field <c>Value</c> ie it is suitable for use when rendering content.</para>
/// <para>If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns <c>default(T)</c>.</para>
/// <para>If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter.</para>
/// <para>The alias is case-insensitive.</para>
/// </remarks>
public static T Value<T>(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false)
public static T Value<T>(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<T>(culture, segment);
return PublishedValueFallback.GetValue<T>(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<T>(culture, segment);
}
// fixme - .Value() refactoring - in progress
public static IHtmlString Value<T>(this IPublishedContent content, string aliases, Func<T, string> format, string alt = "", bool recurse = false)
public static IHtmlString Value<T>(this IPublishedContent content, string aliases, Func<T, string> format, string alt = "", int fallback = 0)
{
var aliasesA = aliases.Split(',');
if (aliasesA.Length == 0)
@@ -1,72 +0,0 @@
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Web
{
/// <summary>
/// Provides extension methods for <c>IPublishedProperty</c>.
/// </summary>
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<T>
public static T Value<T>(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default)
{
// for Value<T> 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<T> it may return Enumerable<T>.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<T>();
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<T>();
//if (convert.Success) return convert.Result;
return defaultValue;
}
#endregion
}
}
+21 -4
View File
@@ -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
/// <param name="alias">The property alias.</param>
/// <param name="culture">The variation language.</param>
/// <param name="segment">The variation segment.</param>
/// <param name="fallback">Optional fallback strategy.</param>
/// <param name="defaultValue">The default value.</param>
/// <returns>The value of the content's property identified by the alias, if it exists, otherwise a default value.</returns>
/// <remarks>
@@ -106,14 +108,21 @@ namespace Umbraco.Web
/// <para>If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter.</para>
/// <para>The alias is case-insensitive.</para>
/// </remarks>
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
/// <param name="alias">The property alias.</param>
/// <param name="culture">The variation language.</param>
/// <param name="segment">The variation segment.</param>
/// <param name="fallback">Optional fallback strategy.</param>
/// <param name="defaultValue">The default value.</param>
/// <returns>The value of the content's property identified by the alias, converted to the specified type.</returns>
/// <remarks>
@@ -136,14 +146,21 @@ namespace Umbraco.Web
/// <para>If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter.</para>
/// <para>The alias is case-insensitive.</para>
/// </remarks>
public static T Value<T>(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default)
public static T Value<T>(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<T>(culture, segment);
return PublishedValueFallback.GetValue<T>(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<T>(culture, segment);
}
#endregion
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Web
{
/// <summary>
/// Provides extension methods for <c>IPublishedProperty</c>.
/// </summary>
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<T>
public static T Value<T>(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<T>();
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<T>();
if (noValueConverted) return noValueConverted.Result;
// cannot cast noValue nor convert it, nothing we can return but 'default'
return default;
}
#endregion
}
}
+10 -2
View File
@@ -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;
+92 -92
View File
@@ -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
// {
// /// <summary>
// /// Helper method to create a root model for a tree
// /// </summary>
// /// <returns></returns>
// 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
{
/// <summary>
/// Helper method to create a root model for a tree
/// </summary>
/// <returns></returns>
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<string>("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<string>("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<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
// .ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue<string>("application"));
// }
// else if (id == "created")
// {
// menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
// .ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue<string>("application"));
// Root actions
if (id == "-1")
{
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
.ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue<string>("application"));
}
else if (id == "created")
{
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
.ConvertLegacyMenuItem(null, Constants.Trees.Packages, queryStrings.GetValue<string>("application"));
// menu.Items.Add<RefreshNode, ActionRefresh>(
// Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
// }
// else
// {
// //it's a package node
// menu.Items.Add<ActionDelete>(Services.TextService.Localize("actions", ActionDelete.Instance.Alias));
// }
menu.Items.Add<RefreshNode, ActionRefresh>(
Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
}
else
{
//it's a package node
menu.Items.Add<ActionDelete>(Services.TextService.Localize("actions", ActionDelete.Instance.Alias));
}
// return menu;
// }
// }
//}
return menu;
}
}
}
+1 -1
View File
@@ -906,7 +906,7 @@
<Compile Include="Models\LoginStatusModel.cs" />
<Compile Include="PropertyEditors\ValueConverters\MarkdownEditorValueConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\TextStringValueConverter.cs" />
<Compile Include="PublishedContentPropertyExtension.cs" />
<Compile Include="PublishedPropertyExtension.cs" />
<Compile Include="Models\UmbracoProperty.cs" />
<Compile Include="Mvc\MergeParentContextViewDataAttribute.cs" />
<Compile Include="Mvc\ViewDataDictionaryExtensions.cs" />
+2 -2
View File
@@ -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;
}