Merge branch 'u4-11502' of https://github.com/AndyButland/Umbraco-CMS into temp8-11502

This commit is contained in:
Stephan
2018-07-18 11:13:22 +02:00
30 changed files with 832 additions and 222 deletions

View File

@@ -113,6 +113,7 @@ namespace Umbraco.Core.Migrations.Upgrade
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}");
//FINAL

View File

@@ -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");
}
}
}

View File

@@ -33,5 +33,11 @@ namespace Umbraco.Core.Models
/// If true, a variant node cannot be published unless this language variant is created
/// </summary>
bool Mandatory { get; set; }
/// <summary>
/// Defines the id of a fallback language that can be used in multi-lingual scenarios to provide
/// content if the requested language does not have it published.
/// </summary>
int? FallbackLanguageId { get; set; }
}
}

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)
{
@@ -32,6 +33,7 @@ namespace Umbraco.Core.Models
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 FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, int?>(x => x.FallbackLanguageId);
}
/// <summary>
@@ -71,5 +73,11 @@ namespace Umbraco.Core.Models
get => _mandatory;
set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector);
}
public int? FallbackLanguageId
{
get => _fallbackLanguageId;
set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector);
}
}
}

View File

@@ -2,6 +2,12 @@
namespace Umbraco.Core.Models.PublishedContent
{
public enum PublishedValueFallbackPriority
{
RecursiveTree,
FallbackLanguage
}
/// <summary>
/// Provides a fallback strategy for getting <see cref="IPublishedElement"/> values.
/// </summary>
@@ -30,8 +36,8 @@ namespace Umbraco.Core.Models.PublishedContent
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);
object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority);
T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse);
T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority);
}
}

View File

@@ -21,9 +21,9 @@
public T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue;
/// <inheritdoc />
public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) => defaultValue;
public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue;
/// <inheritdoc />
public T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue;
public T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue;
}
}
}

View File

@@ -38,5 +38,14 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("mandatory")]
[Constraint(Default = "0")]
public bool Mandatory { get; set; }
/// <summary>
/// Defines the fallback language that can be used in multi-lingual scenarios to provide
/// content if the requested language does not have it published.
/// </summary>
[Column("fallbackLanguageId")]
[ForeignKey(typeof(LanguageDto), Column = "id")]
[NullSetting(NullSetting = NullSettings.Null)]
public int? FallbackLanguageId { get; set; }
}
}

View File

@@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Factories
{
public static ILanguage BuildEntity(LanguageDto dto)
{
var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory };
var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory, FallbackLanguageId = dto.FallbackLanguageId };
// reset dirty initial properties (U4-1946)
lang.ResetDirtyProperties(false);
return lang;
@@ -16,9 +16,11 @@ namespace Umbraco.Core.Persistence.Factories
public static LanguageDto BuildDto(ILanguage entity)
{
var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory };
var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory, FallbackLanguageId = entity.FallbackLanguageId };
if (entity.HasIdentity)
{
dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture));
}
return dto;
}

View File

@@ -52,7 +52,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
sql.OrderBy<LanguageDto>(dto => dto.Id);
// get languages
var languages = Database.Fetch<LanguageDto>(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList();
var dtos = Database.Fetch<LanguageDto>(sql);
var languages = dtos.Select(ConvertFromDto).ToList();
// fix inconsistencies: there has to be a default language, and it has to be mandatory
var defaultLanguage = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage) ?? languages.First();
@@ -79,7 +80,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
@@ -144,14 +146,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
IsolatedCache.ClearAllCache();
}
;
var dto = LanguageFactory.BuildDto(entity);
var id = Convert.ToInt32(Database.Insert(dto));
entity.Id = id;
entity.ResetDirtyProperties();
}
protected override void PersistUpdatedItem(ILanguage entity)
@@ -204,7 +204,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

View File

@@ -335,6 +335,7 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorMacroColumns.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\UserForeignKeys.cs" />
<Compile Include="Models\AuditEntry.cs" />
<Compile Include="Models\Consent.cs" />

View File

@@ -47,6 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.That(language.HasIdentity, Is.True);
Assert.That(language.CultureName, Is.EqualTo("en-US"));
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,7 +185,7 @@ 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
@@ -190,6 +193,7 @@ namespace Umbraco.Tests.Persistence.Repositories
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.IsNull(languageBR.FallbackLanguageId);
}
}
@@ -211,6 +215,31 @@ namespace Umbraco.Tests.Persistence.Repositories
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.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));
}
}
@@ -232,13 +261,11 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.IsTrue(languageBR.Mandatory);
// Act
var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefaultVariantLanguage = true, Mandatory = true };
repository.Save(languageNZ);
languageBR = repository.Get(languageBR.Id);
// Assert
Assert.IsFalse(languageBR.IsDefaultVariantLanguage);
Assert.IsTrue(languageNZ.IsDefaultVariantLanguage);
}
@@ -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));
}
}
@@ -314,7 +343,7 @@ namespace Umbraco.Tests.Persistence.Repositories
base.TearDown();
}
public void CreateTestData()
private void CreateTestData()
{
var languageDK = new Language("da-DK") { CultureName = "da-DK" };
ServiceContext.LocalizationService.Save(languageDK);//Id 2

View File

@@ -0,0 +1,130 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Tests.Testing;
using Umbraco.Web;
namespace Umbraco.Tests.PublishedContent
{
[TestFixture]
[UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)]
public class PublishedContentLanuageVariantTests : 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.
var languages = new List<Language>
{
new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = 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" }
};
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),
};
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), props);
var prop1 = new SolidPublishedPropertyWithLanguageVariants
{
Alias = "welcomeText",
};
prop1.SetSourceValue("en-US", "Welcome");
prop1.SetValue("en-US", "Welcome");
prop1.SetSourceValue("de", "Willkommen");
prop1.SetValue("de", "Willkommen");
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>
{
prop1
}
});
}
[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 Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "es");
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");
Assert.AreEqual("Welcome", value);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -34,11 +34,11 @@ namespace Umbraco.Tests.PublishedContent
Container.RegisterSingleton<IPublishedModelFactory>(f => new PublishedModelFactory(f.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
Container.RegisterSingleton<IPublishedContentTypeFactory, PublishedContentTypeFactory>();
Container.RegisterSingleton<IPublishedValueFallback, PublishedValueFallback>();
Container.RegisterSingleton<IPublishedValueFallback, PublishedValueLanguageFallback>();
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 },
@@ -333,7 +333,7 @@ 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);

View File

@@ -257,10 +257,72 @@ 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)
{
_solidSourceValues.Add(culture, value);
}
public void SetValue(string culture, object value)
{
_solidValues.Add(culture, value);
}
public void SetXPathValue(string culture, object value)
{
_solidXPathValues.Add(culture, value);
}
}
[PublishedModel("ContentType2")]

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Tests.TestHelpers
{
base.Compose();
Container.RegisterSingleton<IPublishedValueFallback, PublishedValueFallback>();
Container.RegisterSingleton<IPublishedValueFallback, PublishedValueLanguageFallback>();
Container.RegisterSingleton<ProfilingLogger>();
}

View File

@@ -122,6 +122,8 @@
<Compile Include="Migrations\MigrationTests.cs" />
<Compile Include="Models\PathValidationTests.cs" />
<Compile Include="Models\VariationTests.cs" />
<Compile Include="PublishedContent\PublishedContentLanuageVariantTests.cs" />
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
<Compile Include="PublishedContent\NuCacheTests.cs" />
<Compile Include="Testing\Objects\TestDataSource.cs" />

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
@@ -22,17 +32,19 @@
"treeHeaders_languages",
"general_mandatory",
"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;
});

View File

@@ -36,6 +36,7 @@
<span ng-if="language.isDefault">- {{vm.labels.general}}</span>
</td>
<td><span ng-if="language.isMandatory">{{vm.labels.mandatory}}</span></td>
<td><span ng-if="language.fallbackLanguageId">{{vm.labels.fallsbackTo}}: {{vm.getLanguageById(language.fallbackLanguageId).name}}</span></td>
<td style="text-align: right;">
<umb-button
ng-if="!language.isDefault"

View File

@@ -1691,10 +1691,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</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 langugae 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">

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
@@ -8,8 +7,6 @@ using System.Web.Http;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
@@ -69,12 +66,11 @@ namespace Umbraco.Web.Editors
}
else if (allLangs.All(x => !x.IsDefaultVariantLanguage))
{
//if no language has the default flag, then the defaul language is the one with the lowest id
//if no language has the default flag, then the default language is the one with the lowest id
model.IsDefaultVariantLanguage = allLangs[0].Id == lang.Id;
model.Mandatory = allLangs[0].Id == lang.Id;
}
}
return model;
}
@@ -88,13 +84,23 @@ 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();
}
var totalLangs = Services.LocalizationService.GetAllLanguages().Count();
var langs = Services.LocalizationService.GetAllLanguages().ToArray();
var totalLangs = langs.Length;
if (language.IsDefaultVariantLanguage || totalLangs == 1)
{
var message = $"Language '{language.IsoCode}' is currently set to 'default' or it is the only installed language and can not be deleted.";
var message = $"Language '{language.CultureName}' is currently set to 'default' or it is the only installed language and cannot be deleted.";
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message));
}
if (langs.Any(x => x.FallbackLanguageId.HasValue && x.FallbackLanguageId.Value == language.Id))
{
var message = $"Language '{language.CultureName}' is defined as a fall-back language for one or more other languages, and so cannot be deleted.";
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message));
}
@@ -136,21 +142,62 @@ namespace Umbraco.Web.Editors
}
//create it
var newLang = new Umbraco.Core.Models.Language(culture.Name)
var newLang = new Core.Models.Language(culture.Name)
{
CultureName = culture.DisplayName,
IsDefaultVariantLanguage = language.IsDefaultVariantLanguage,
Mandatory = language.Mandatory
Mandatory = language.Mandatory,
FallbackLanguageId = language.FallbackLanguageId
};
Services.LocalizationService.Save(newLang);
return Mapper.Map<Language>(newLang);
}
found.Mandatory = language.Mandatory;
found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage;
found.FallbackLanguageId = language.FallbackLanguageId;
string selectedFallbackLanguageCultureName;
if (DoesUpdatedFallbackLanguageCreateACircularPath(found, out selectedFallbackLanguageCultureName))
{
ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + selectedFallbackLanguageCultureName + "' would create a circular path.");
throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState));
}
Services.LocalizationService.Save(found);
return Mapper.Map<Language>(found);
}
private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language, out string selectedFallbackLanguageCultureName)
{
if (language.FallbackLanguageId.HasValue == false)
{
selectedFallbackLanguageCultureName = string.Empty;
return false;
}
var languages = Services.LocalizationService.GetAllLanguages().ToArray();
var fallbackLanguageId = language.FallbackLanguageId;
while (fallbackLanguageId.HasValue)
{
if (fallbackLanguageId.Value == language.Id)
{
// We've found the current language in the path of fall back languages, so we have a circular path.
selectedFallbackLanguageCultureName = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).CultureName;
return true;
}
fallbackLanguageId = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).FallbackLanguageId;
}
selectedFallbackLanguageCultureName = string.Empty;
return false;
}
private static ILanguage GetLanguageFromCollectionById(IEnumerable<ILanguage> languages, int id)
{
return languages.Single(x => x.Id == id);
}
}
}

View File

@@ -24,5 +24,8 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "isMandatory")]
public bool Mandatory { get; set; }
[DataMember(Name = "fallbackLanguageId")]
public int? FallbackLanguageId { get; set; }
}
}

View File

@@ -11,45 +11,45 @@ namespace Umbraco.Web.Models.PublishedContent
// kinda reproducing what was available in v7
/// <inheritdoc />
public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue)
public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue)
{
// no fallback here
return defaultValue;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue)
public virtual T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue)
{
// no fallback here
return defaultValue;
}
/// <inheritdoc />
public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue)
public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue)
{
// no fallback here
return defaultValue;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue)
public virtual T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue)
{
// no fallback here
return defaultValue;
}
/// <inheritdoc />
public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse)
public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority)
{
// no fallback here
if (!recurse) return defaultValue;
// is that ok?
return GetValue<object>(content, alias, culture, segment, defaultValue, recurse);
return GetValue<object>(content, alias, culture, segment, defaultValue, true, fallbackPriority);
}
/// <inheritdoc />
public T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse)
public virtual T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority)
{
// no fallback here
if (!recurse) return defaultValue;

View File

@@ -0,0 +1,218 @@
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
namespace Umbraco.Web.Models.PublishedContent
{
/// <summary>
/// Provides a default implementation for <see cref="IPublishedValueFallback"/> that allows
/// for use of fall-back languages
/// </summary>
/// <remarks>
/// Inherits from <see cref="PublishedValueFallback" /> that implments what was available in v7.
/// </remarks>
public class PublishedValueLanguageFallback : PublishedValueFallback
{
private readonly ILocalizationService _localizationService;
public PublishedValueLanguageFallback(ServiceContext services)
{
_localizationService = services.LocalizationService;
}
/// <inheritdoc />
public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue)
{
object value;
if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value))
{
return value;
}
return base.GetValue(property, culture, segment, defaultValue);
}
/// <inheritdoc />
public override T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue)
{
T value;
if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value))
{
return value;
}
return base.GetValue(property, culture, segment, defaultValue);
}
/// <inheritdoc />
public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue)
{
object value;
if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value))
{
return value;
}
return base.GetValue(content, alias, culture, segment, defaultValue);
}
/// <inheritdoc />
public override T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue)
{
T value;
if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value))
{
return value;
}
return base.GetValue(content, alias, culture, segment, defaultValue);
}
/// <inheritdoc />
public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority)
{
return GetValue<object>(content, alias, culture, segment, defaultValue, recurse, fallbackPriority);
}
/// <inheritdoc />
public override T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority)
{
if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree)
{
var result = base.GetValue<T>(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree);
if (ValueIsNotNullEmptyOrDefault(result, defaultValue))
{
// We've prioritised recursive tree search and found a value, so can return it.
return result;
}
if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result))
{
return result;
}
return defaultValue;
}
if (fallbackPriority == PublishedValueFallbackPriority.FallbackLanguage)
{
T result;
if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result))
{
return result;
}
}
// No language fall back content found, so use base implementation
return base.GetValue<T>(content, alias, culture, segment, defaultValue, recurse, fallbackPriority);
}
private bool TryGetValueFromFallbackLanguage<T>(IPublishedProperty property, string culture, string segment, T defaultValue, out T value)
{
if (string.IsNullOrEmpty(culture))
{
value = defaultValue;
return false;
}
var language = _localizationService.GetLanguageByIsoCode(culture);
if (language.FallbackLanguageId.HasValue == false)
{
value = defaultValue;
return false;
}
var fallbackLanguageId = language.FallbackLanguageId;
while (fallbackLanguageId.HasValue)
{
var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value);
value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue);
if (ValueIsNotNullEmptyOrDefault(value, defaultValue))
{
return true;
}
fallbackLanguageId = fallbackLanguage.FallbackLanguageId;
}
value = defaultValue;
return false;
}
private bool TryGetValueFromFallbackLanguage<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value)
{
if (string.IsNullOrEmpty(culture))
{
value = defaultValue;
return false;
}
var language = _localizationService.GetLanguageByIsoCode(culture);
if (language.FallbackLanguageId.HasValue == false)
{
value = defaultValue;
return false;
}
var fallbackLanguageId = language.FallbackLanguageId;
while (fallbackLanguageId.HasValue)
{
var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value);
value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue);
if (ValueIsNotNullEmptyOrDefault(value, defaultValue))
{
return true;
}
fallbackLanguageId = fallbackLanguage.FallbackLanguageId;
}
value = defaultValue;
return false;
}
private bool TryGetValueFromFallbackLanguage<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, out T value)
{
if (string.IsNullOrEmpty(culture))
{
value = defaultValue;
return false;
}
var language = _localizationService.GetLanguageByIsoCode(culture);
if (language.FallbackLanguageId.HasValue == false)
{
value = defaultValue;
return false;
}
var fallbackLanguageId = language.FallbackLanguageId;
while (fallbackLanguageId.HasValue)
{
var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value);
value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse);
if (ValueIsNotNullEmptyOrDefault(value, defaultValue))
{
return true;
}
fallbackLanguageId = fallbackLanguage.FallbackLanguageId;
}
value = defaultValue;
return false;
}
private ILanguage GetLanguageById(int id)
{
return _localizationService.GetLanguageById(id);
}
private static bool ValueIsNotNullEmptyOrDefault<T>(T value, T defaultValue)
{
return value != null &&
string.IsNullOrEmpty(value.ToString()) == false &&
value.Equals(defaultValue) == false;
}
}
}

View File

@@ -152,8 +152,9 @@ 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="recurse">A value indicating whether to recurse.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="recurse">A value indicating whether to recurse.</param>
/// <param name="fallbackPriority">Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used.</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>
@@ -162,14 +163,14 @@ 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 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, object defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree)
{
var property = content.GetProperty(alias);
if (property != null && property.HasValue(culture, segment))
return property.GetValue(culture, segment);
return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse);
return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority);
}
#endregion
@@ -186,6 +187,7 @@ namespace Umbraco.Web
/// <param name="segment">The variation segment.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="recurse">A value indicating whether to recurse.</param>
/// <param name="fallbackPriority">Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used.</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>
@@ -194,18 +196,18 @@ 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 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, T defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree)
{
var property = content.GetProperty(alias);
if (property != null && property.HasValue(culture, segment))
return property.Value<T>(culture, segment);
return PublishedValueFallback.GetValue<T>(content, alias, culture, segment, defaultValue, recurse);
return PublishedValueFallback.GetValue<T>(content, alias, culture, segment, defaultValue, recurse, fallbackPriority);
}
// 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 = "", bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree)
{
var aliasesA = aliases.Split(',');
if (aliasesA.Length == 0)

View File

@@ -199,7 +199,7 @@ namespace Umbraco.Web.Runtime
composition.Container.Register(_ => GlobalHost.ConnectionManager.GetHubContext<PreviewHub>(), new PerContainerLifetime());
// register properties fallback
composition.Container.RegisterSingleton<IPublishedValueFallback, PublishedValueFallback>();
composition.Container.RegisterSingleton<IPublishedValueFallback, PublishedValueLanguageFallback>();
}
internal void Initialize(

View File

@@ -263,6 +263,7 @@
<Compile Include="Models\Mapping\UserGroupDefaultPermissionsResolver.cs" />
<Compile Include="Models\Mapping\ContentItemDisplayVariationResolver.cs" />
<Compile Include="Models\PublishedContent\HttpContextVariationContextAccessor.cs" />
<Compile Include="Models\PublishedContent\PublishedValueLanguageFallback.cs" />
<Compile Include="Models\PublishedContent\PublishedValueFallback.cs" />
<Compile Include="Models\RelatedLink.cs" />
<Compile Include="Models\RelatedLinkBase.cs" />