From 592de8bebcba97441a0996f32bbc09102bbadea3 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 5 Jul 2018 16:00:53 +0200 Subject: [PATCH 01/12] Model, retrieval, mapping and display for fall back language editing --- .../Upgrade/V_8_0_0/FallbackLanguage.cs | 24 +++++++++++++++++++ src/Umbraco.Core/Models/ILanguage.cs | 6 +++++ src/Umbraco.Core/Models/Language.cs | 8 +++++++ .../Persistence/Dtos/LanguageDto.cs | 9 +++++++ .../Implement/LanguageRepository.cs | 21 +++++++++++++--- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../src/views/languages/edit.controller.js | 14 +++++++++++ .../src/views/languages/edit.html | 15 ++++++++++-- .../views/languages/overview.controller.js | 2 ++ .../src/views/languages/overview.html | 1 + .../Umbraco/config/lang/en_us.xml | 1 + .../Models/ContentEditing/Language.cs | 3 +++ 12 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs new file mode 100644 index 0000000000..f0d7c02b82 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + /// + /// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for + /// a given language. + /// + public class FallbackLanguage : MigrationBase + { + public FallbackLanguage(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) + AddColumn("fallbackLanguageId"); + } + } +} diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 7bf9e9b32c..f02bd33d2b 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -33,5 +33,11 @@ namespace Umbraco.Core.Models /// If true, a variant node cannot be published unless this language variant is created /// bool Mandatory { get; set; } + + /// + /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// content if the requested language does not have it published. + /// + ILanguage FallbackLanguage { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index fa1c9dc826..42c305d492 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Models private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; + private ILanguage _fallbackLanguage; public Language(string isoCode) { @@ -32,6 +33,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); + public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguage); } /// @@ -71,5 +73,11 @@ namespace Umbraco.Core.Models get => _mandatory; set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } + + public ILanguage FallbackLanguage + { + get => _fallbackLanguage; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguage, Ps.Value.FallbackLanguageSelector); + } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 12c9fd0bd4..f69caf6c91 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -38,5 +38,14 @@ namespace Umbraco.Core.Persistence.Dtos [Column("mandatory")] [Constraint(Default = "0")] public bool Mandatory { get; set; } + + /// + /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// content if the requested language does not have it published. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index db2e1124a2..9566247f96 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -52,7 +52,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql.OrderBy(dto => dto.Id); // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).ToList(); + var dtos = Database.Fetch(sql); + var languages = dtos.Select(ConvertFromDto).ToList(); + PopulateFallbackLanguages(dtos, languages); // initialize the code-id map lock (_codeIdMap) @@ -74,7 +76,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConvertFromDto); + var dtos = Database.Fetch(sql); + var languages = dtos.Select(ConvertFromDto).ToList(); + PopulateFallbackLanguages(dtos, languages); + return languages; } #endregion @@ -199,7 +204,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = LanguageFactory.BuildEntity(dto); return entity; } - + + private static void PopulateFallbackLanguages(List dtos, IList languages) + { + foreach (var dto in dtos.Where(x => x.FallbackLanguageId.HasValue)) + { + var language = languages.Single(x => x.Id == dto.Id); + // ReSharper disable once PossibleInvalidOperationException (DTOs with fallback languages have already been filtered in the loop condition) + language.FallbackLanguage = languages.Single(x => x.Id == dto.FallbackLanguageId.Value); + } + } + public ILanguage GetByIsoCode(string isoCode) { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6bd78044f8..67028568eb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -334,6 +334,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index afb5333ded..9b554b1785 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -56,6 +56,20 @@ }); + $scope.properties = { + fallbackLanguage: { + alias: "fallbackLanguage", + description: "To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.", + label: "Fall back language" + } + }; + + vm.loading = true; + languageResource.getAll().then(function (languages) { + vm.availableLanguages = languages; + vm.loading = false; + }); + if(!$routeParams.create) { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 6aaf915960..adc38de05a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -14,11 +14,11 @@ hide-alias="true"> - + - + @@ -64,6 +64,17 @@ + +
+ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index c81f93c7d6..2f116ef3cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -22,12 +22,14 @@ "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; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index 90764d3f67..f53326a491 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -36,6 +36,7 @@ - {{vm.labels.general}} {{vm.labels.mandatory}} + {{vm.labels.fallsbackTo}}: {{language.fallbackLanguage.name}} Default language An Umbraco site can only have one default langugae set. Switching default language may result in default content missing. + Falls back to diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index f78d2bd28f..309e111e32 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -24,5 +24,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "isMandatory")] public bool Mandatory { get; set; } + + [DataMember(Name = "fallbackLanguage")] + public Language FallbackLanguage { get; set; } } } From 660fe2d7732f32b5c43999666f20141dc0fd1061 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 5 Jul 2018 19:37:59 +0200 Subject: [PATCH 02/12] Localised fallback language related messages. Filtered out current languge from list of available fall back languages for selection. --- .../src/views/languages/edit.controller.js | 28 +++++++++++-------- .../src/views/languages/edit.html | 2 +- .../Umbraco/config/lang/en_us.xml | 5 +++- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 9b554b1785..3a4961ca81 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -29,7 +29,10 @@ "languages_mandatoryLanguageHelp", "languages_defaultLanguage", "languages_defaultLanguageHelp", - "languages_addLanguage" + "languages_addLanguage", + "languages_noFallbackLanguageOption", + "languages_fallbackLanguageDescription", + "languages_fallbackLanguage" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -39,6 +42,15 @@ vm.labels.defaultLanguage = values[3]; vm.labels.defaultLanguageHelp = values[4]; vm.labels.addLanguage = values[5]; + vm.labels.noFallbackLanguageOption = values[6]; + + $scope.properties = { + fallbackLanguage: { + alias: "fallbackLanguage", + description: values[7], + label: values[8] + } + }; if($routeParams.create) { vm.page.name = vm.labels.addLanguage; @@ -56,21 +68,15 @@ }); - $scope.properties = { - fallbackLanguage: { - alias: "fallbackLanguage", - description: "To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.", - label: "Fall back language" - } - }; - vm.loading = true; languageResource.getAll().then(function (languages) { - vm.availableLanguages = languages; + vm.availableLanguages = languages.filter(function (l) { + return $routeParams.id != l.id; + }); vm.loading = false; }); - if(!$routeParams.create) { + if (!$routeParams.create) { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index adc38de05a..9ebbc5aede 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -70,7 +70,7 @@ ng-model="vm.language.fallbackLanguage.id" required ng-options="l.id as l.name for l in vm.availableLanguages"> - + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 555d4edfb4..ea1db802d5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1657,11 +1657,14 @@ To manage your website, simply open the Umbraco back office and start adding con Add language Mandatory - Properties on this language has to be filled out before the node can be published. + Properties on this language have to be filled out before the node can be published. Default language An Umbraco site can only have one default langugae set. Switching default language may result in default content missing. Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language From 53e96b25f686b01bac86c8a6f86c84b8df69434f Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 15:23:21 +0200 Subject: [PATCH 03/12] Save of fall-back language on create and update of language --- .../Persistence/Factories/LanguageFactory.cs | 7 +++++ .../Implement/LanguageRepository.cs | 2 -- .../src/views/languages/edit.controller.js | 18 ++++++++++-- .../src/views/languages/edit.html | 1 - .../views/languages/overview.controller.js | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 28 ++++++++++++++----- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index 7b24411498..c805ae7f5a 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -18,7 +18,14 @@ namespace Umbraco.Core.Persistence.Factories { var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory }; if (entity.HasIdentity) + { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); + } + + if (entity.FallbackLanguage != null) + { + dto.FallbackLanguageId = entity.FallbackLanguage.Id; + } return dto; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 9566247f96..96bb088f2b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -144,14 +144,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) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 3a4961ca81..bc5c421b7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -52,7 +52,7 @@ } }; - if($routeParams.create) { + if ($routeParams.create) { vm.page.name = vm.labels.addLanguage; languageResource.getCultures().then(function (culturesDictionary) { var cultures = []; @@ -65,7 +65,6 @@ vm.availableCultures = cultures; }); } - }); vm.loading = true; @@ -99,11 +98,26 @@ }); } + function setCultureForFallbackLanguage(lang) { + for (var i = 0; i < vm.availableLanguages.length; i++) { + if (vm.availableLanguages[i].id === lang.id) { + lang.culture = vm.availableLanguages[i].culture; + break; + } + } + } + function save() { if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; + // We need to attach the ISO code to the fall-back language to pass + // server-side validation. + if (vm.language.fallbackLanguage) { + setCultureForFallbackLanguage(vm.language.fallbackLanguage); + } + languageResource.save(vm.language).then(function (lang) { formHelper.resetForm({ scope: $scope }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 9ebbc5aede..5570b901e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -68,7 +68,6 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index 2f116ef3cb..c8a728d3aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -34,7 +34,7 @@ vm.page.name = vm.labels.languages; }); - languageResource.getAll().then(function(languages) { + languageResource.getAll().then(function (languages) { vm.languages = languages; vm.loading = false; }); diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 96019da702..418c8401ff 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -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; @@ -136,21 +133,38 @@ 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, }; + + AssociateFallbackLanguage(language, newLang); Services.LocalizationService.Save(newLang); return Mapper.Map(newLang); } found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; + AssociateFallbackLanguage(language, found); Services.LocalizationService.Save(found); return Mapper.Map(found); } - + + private static void AssociateFallbackLanguage(Language submittedLanguage, ILanguage languageToCreateOrUpdate) + { + if (submittedLanguage.FallbackLanguage == null) + { + return; + } + + var fallbackLanguageCulture = CultureInfo.GetCultureInfo(submittedLanguage.FallbackLanguage.IsoCode); + languageToCreateOrUpdate.FallbackLanguage = new Core.Models.Language(fallbackLanguageCulture.Name) + { + Id = submittedLanguage.FallbackLanguage.Id, + CultureName = fallbackLanguageCulture.DisplayName + }; + } } } From 8a34ec886470146ffdbd34cd3060991ce277eb22 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 15:51:13 +0200 Subject: [PATCH 04/12] Prevented creation of circular fall-back language paths. Allowed for update to no fall-back language. --- .../src/views/languages/edit.controller.js | 5 ++++ src/Umbraco.Web/Editors/LanguageController.cs | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index bc5c421b7b..79972725fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -112,6 +112,11 @@ if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; + // Handle selection of no fall-back language (should pass null) + if (!vm.language.fallbackLanguage.id) { + vm.language.fallbackLanguage = null; + } + // We need to attach the ISO code to the fall-back language to pass // server-side validation. if (vm.language.fallbackLanguage) { diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 418c8401ff..76a40ac329 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -148,6 +148,13 @@ namespace Umbraco.Web.Editors found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; AssociateFallbackLanguage(language, found); + + if (UpdatedFallbackLanguageCreatesCircularPath(found)) + { + ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + Services.LocalizationService.Save(found); return Mapper.Map(found); } @@ -156,6 +163,7 @@ namespace Umbraco.Web.Editors { if (submittedLanguage.FallbackLanguage == null) { + languageToCreateOrUpdate.FallbackLanguage = null; return; } @@ -166,5 +174,27 @@ namespace Umbraco.Web.Editors CultureName = fallbackLanguageCulture.DisplayName }; } + + private bool UpdatedFallbackLanguageCreatesCircularPath(ILanguage language) + { + if (language.FallbackLanguage == null) + { + return false; + } + + var languages = Services.LocalizationService.GetAllLanguages().ToArray(); + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + if (fallbackLanguage.Id == language.Id) + { + return true; + } + + fallbackLanguage = languages.Single(x => x.Id == fallbackLanguage.Id).FallbackLanguage; + } + + return false; + } } } From 4e859cd88bd0e23e72cfbf3b169d6304d8ebf4cd Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 16:01:54 +0200 Subject: [PATCH 05/12] Prevent delete of a language used as a fall-back for another language --- src/Umbraco.Web/Editors/LanguageController.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 76a40ac329..0640d16129 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -85,13 +85,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 (language.FallbackLanguage != null && langs.Any(x => x.FallbackLanguage?.Id == 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)); } From 3c13b7bd70e79338f2f00aba79104c59472b2a3e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 23:30:57 +0200 Subject: [PATCH 06/12] Added and updated tests for fall back languages --- .../Persistence/Dtos/LanguageDto.cs | 1 + .../Repositories/LanguageRepositoryTest.cs | 39 ++++++++++++++++--- src/Umbraco.Web/Editors/LanguageController.cs | 8 ++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index f69caf6c91..25ca43f918 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -46,6 +46,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index cd1fe47f39..bda899789d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -47,6 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo("en-US")); Assert.That(language.IsoCode, Is.EqualTo("en-US")); + Assert.That(language.FallbackLanguage, 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, + FallbackLanguage = repository.Get(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.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -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.FallbackLanguage); } } @@ -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.FallbackLanguage); + } + } + + [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", + FallbackLanguage = repository.Get(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.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -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.FallbackLanguage = repository.Get(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.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -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 diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 0640d16129..9b73b50300 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -66,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; } @@ -159,7 +158,7 @@ namespace Umbraco.Web.Editors found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; AssociateFallbackLanguage(language, found); - if (UpdatedFallbackLanguageCreatesCircularPath(found)) + if (DoesUpdatedFallbackLanguageCreateACircularPath(found)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); @@ -185,7 +184,7 @@ namespace Umbraco.Web.Editors }; } - private bool UpdatedFallbackLanguageCreatesCircularPath(ILanguage language) + private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language) { if (language.FallbackLanguage == null) { @@ -198,6 +197,7 @@ namespace Umbraco.Web.Editors { if (fallbackLanguage.Id == language.Id) { + // We've found the current language in the path of fall back languages, so we have a circular path. return true; } From aadd843c332c9120064503d9ac2104dd75b8b769 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 8 Jul 2018 15:17:38 +0200 Subject: [PATCH 07/12] Implemented published value fall back via language --- .../IPublishedValueFallback.cs | 10 +- .../NoopPublishedValueFallback.cs | 6 +- .../PublishedContent/PublishedContentTests.cs | 6 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 +- .../PublishedValueFallback.cs | 14 +- .../PublishedValueLanguageFallback.cs | 219 ++++++++++++++++++ src/Umbraco.Web/PublishedContentExtensions.cs | 14 +- .../Runtime/WebRuntimeComponent.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 9 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 8e1dcfd543..f154d9ef27 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -2,6 +2,12 @@ namespace Umbraco.Core.Models.PublishedContent { + public enum PublishedValueFallbackPriority + { + RecursiveTree, + FallbackLanguage + } + /// /// Provides a fallback strategy for getting values. /// @@ -30,8 +36,8 @@ namespace Umbraco.Core.Models.PublishedContent T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index b99b4ad415..75ab9df35a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -21,9 +21,9 @@ public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; /// - 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; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 02f58a00a3..a09cf6d4ad 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -34,11 +34,11 @@ namespace Umbraco.Tests.PublishedContent Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); Container.RegisterSingleton(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(logger)) { Id = 1}, + new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, new DataType(new RichTextPropertyEditor(logger)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, @@ -323,7 +323,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); diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 5eea6bcf72..2f7fe8700b 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.TestHelpers { base.Compose(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); Container.RegisterSingleton(); } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 47e4b3d872..562b8e393b 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -11,45 +11,45 @@ namespace Umbraco.Web.Models.PublishedContent // kinda reproducing what was available in v7 /// - 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; } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - 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; } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - 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(content, alias, culture, segment, defaultValue, recurse); + return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority); } /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) + public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) { // no fallback here if (!recurse) return defaultValue; diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs new file mode 100644 index 0000000000..c404288a0e --- /dev/null +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -0,0 +1,219 @@ +using LightInject; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Models.PublishedContent +{ + /// + /// Provides a default implementation for that allows + /// for use of fall-back languages + /// + /// + /// Inherits from that implments what was available in v7. + /// + public class PublishedValueLanguageFallback : PublishedValueFallback + { + /// + /// Gets or sets the services context. + /// + [Inject] + public ServiceContext Services { get; set; } + + /// + 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); + } + + /// + public override T GetValue(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); + } + + /// + 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); + } + + /// + public override T GetValue(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); + } + + /// + public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + { + return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + } + + /// + public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + { + if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree) + { + var result = base.GetValue(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(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + } + + private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) + { + return value != null && + string.IsNullOrEmpty(value.ToString()) == false && + value.Equals(defaultValue) == false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private static ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage, ILocalizationService localizationService) + { + fallbackLanguage = localizationService.GetLanguageById(fallbackLanguage.Id); // Ensures reference to next fall-back language is loaded if it exists + return fallbackLanguage.FallbackLanguage; + } + } +} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 1adfb55ca9..22a0bc8aca 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -159,8 +159,9 @@ namespace Umbraco.Web /// The property alias. /// The variation language. /// The variation segment. - /// A value indicating whether to recurse. /// The default value. + /// A value indicating whether to recurse. + /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -169,14 +170,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this 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 @@ -193,6 +194,7 @@ namespace Umbraco.Web /// The variation segment. /// The default value. /// A value indicating whether to recurse. + /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. /// The value of the content's property identified by the alias, converted to the specified type. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -201,18 +203,18 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, 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(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index a4e5db0767..501fb6445d 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -199,7 +199,7 @@ namespace Umbraco.Web.Runtime composition.Container.Register(_ => GlobalHost.ConnectionManager.GetHubContext(), new PerContainerLifetime()); // register properties fallback - composition.Container.RegisterSingleton(); + composition.Container.RegisterSingleton(); } internal void Initialize( diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f266213da0..e0f2554412 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -253,6 +253,7 @@ + From 11871dc4f7d6565022c0174b8ace37d1ef7a0563 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 8 Jul 2018 16:11:26 +0200 Subject: [PATCH 08/12] Changed the services reference in the published value fall back via language implementation to constructor injection --- .../PublishedValueLanguageFallback.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index c404288a0e..e6902c00db 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -1,5 +1,4 @@ -using LightInject; -using Umbraco.Core.Models; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -14,11 +13,12 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueLanguageFallback : PublishedValueFallback { - /// - /// Gets or sets the services context. - /// - [Inject] - public ServiceContext Services { get; set; } + private readonly ILocalizationService _localizationService; + + public PublishedValueLanguageFallback(ILocalizationService localizationService) + { + _localizationService = localizationService; + } /// public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) @@ -122,8 +122,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -139,7 +138,7 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; @@ -154,8 +153,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -171,7 +169,7 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; @@ -186,8 +184,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -203,16 +200,18 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; return false; } - private static ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage, ILocalizationService localizationService) + private ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage) { - fallbackLanguage = localizationService.GetLanguageById(fallbackLanguage.Id); // Ensures reference to next fall-back language is loaded if it exists + // Ensure reference to next fall-back language is loaded if it exists + fallbackLanguage = _localizationService.GetLanguageById(fallbackLanguage.Id); + return fallbackLanguage.FallbackLanguage; } } From f868bc9589e679aa8cc280f469fb1f73de1299b8 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 11 Jul 2018 09:23:26 +0200 Subject: [PATCH 09/12] Added fall back language database migration to UmbracoPlan --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index b7a77b10ce..9db4710241 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -123,7 +123,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{3608CD41-792A-4E9A-A97D-42A5E797EE31}"); // must chain to v8 final state (see at end of file) - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // UPGRADE FROM 7, MORE RECENT @@ -221,10 +221,13 @@ namespace Umbraco.Core.Migrations.Upgrade //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one - need a path to final state Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + // 8.0.0 + Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + // FINAL STATE - MUST MATCH LAST ONE ABOVE ! // whenever this changes, update all references in this file! - Add(string.Empty, "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Add(string.Empty, "{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); } } } From 50c5b2ed9213425db1d6d9b1741868b0ae03458e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 11 Jul 2018 22:15:58 +0100 Subject: [PATCH 10/12] Added test for fallback languages --- .../PublishedContentLanuageVariantTests.cs | 130 ++++++++++ .../PublishedContentMoreTests.cs | 239 ++++++------------ .../PublishedContentSnapshotTestBase.cs | 100 ++++++++ .../SolidPublishedSnapshot.cs | 70 ++++- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../PublishedValueLanguageFallback.cs | 4 +- 6 files changed, 376 insertions(+), 169 deletions(-) create mode 100644 src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs create mode 100644 src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs new file mode 100644 index 0000000000..ea77310977 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -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 + { + new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, + new Language("fr") { Id = 2, CultureName = "French" }, + new Language("es") { Id = 3, CultureName = "Spanish" }, + new Language("it") { Id = 4, CultureName = "Italian" }, + new Language("de") { Id = 5, CultureName = "German" } + }; + languages[2].FallbackLanguage = languages[0]; + languages[3].FallbackLanguage = languages[2]; + + var localizationService = Mock.Get(serviceContext.LocalizationService); + localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); + localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) + .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); + } + + internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) + { + var props = new[] + { + factory.CreatePropertyType("prop1", 1), + }; + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), 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 + { + 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); + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 101c7827c3..22965ac141 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -1,91 +1,94 @@ -using System; +using System.Collections.ObjectModel; using System.Linq; -using System.Collections.ObjectModel; -using System.Web.Routing; -using Moq; using NUnit.Framework; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using Umbraco.Core.Composing; -using Current = Umbraco.Core.Composing.Current; -using LightInject; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.Accessors; namespace Umbraco.Tests.PublishedContent { [TestFixture] [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] - public class PublishedContentMoreTests : PublishedContentTestBase + public class PublishedContentMoreTests : PublishedContentSnapshotTestBase { - // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet - // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx - // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx - - public override void SetUp() + internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) { - base.SetUp(); + var props = new[] + { + factory.CreatePropertyType("prop1", 1), + }; + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); + var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), props); + var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), props); - var umbracoContext = GetUmbracoContext(); - Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; - } + cache.Add(new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlSegment = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - protected override void Compose() - { - base.Compose(); + cache.Add(new SolidPublishedContent(contentType2) + { + Id = 2, + SortOrder = 1, + Name = "Content 2", + UrlSegment = "content-2", + Path = "/2", + Level = 1, + Url = "/content-2", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); - } - - protected override TypeLoader CreatePluginManager(IServiceFactory f) - { - var pluginManager = base.CreatePluginManager(f); - - // this is so the model factory looks into the test assembly - pluginManager.AssembliesToScan = pluginManager.AssembliesToScan - .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) - .ToList(); - - return pluginManager; - } - - private UmbracoContext GetUmbracoContext() - { - RouteData routeData = null; - - var publishedSnapshot = CreatePublishedSnapshot(); - - var publishedSnapshotService = new Mock(); - publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); - - var globalSettings = TestObjects.GetGlobalSettings(); - - var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; - var umbracoContext = new UmbracoContext( - httpContext, - publishedSnapshotService.Object, - new WebSecurity(httpContext, Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), - Enumerable.Empty(), - globalSettings, - new TestVariationContextAccessor()); - - return umbracoContext; - } - - public override void TearDown() - { - base.TearDown(); - - Current.Reset(); + cache.Add(new SolidPublishedContent(contentType2Sub) + { + Id = 3, + SortOrder = 2, + Name = "Content 2Sub", + UrlSegment = "content-2sub", + Path = "/3", + Level = 1, + Url = "/content-2sub", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); } [Test] @@ -197,95 +200,5 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1, result[0].Id); Assert.AreEqual(2, result[1].Id); } - - private static SolidPublishedSnapshot CreatePublishedSnapshot() - { - var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); - - var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); - var caches = new SolidPublishedSnapshot(); - var cache = caches.InnerContentCache; - - var props = new[] - { - factory.CreatePropertyType("prop1", 1), - }; - - var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); - var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), props); - var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), props); - - cache.Add(new SolidPublishedContent(contentType1) - { - Id = 1, - SortOrder = 0, - Name = "Content 1", - UrlSegment = "content-1", - Path = "/1", - Level = 1, - Url = "/content-1", - ParentId = -1, - ChildIds = new int[] {}, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2) - { - Id = 2, - SortOrder = 1, - Name = "Content 2", - UrlSegment = "content-2", - Path = "/2", - Level = 1, - Url = "/content-2", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2Sub) - { - Id = 3, - SortOrder = 2, - Name = "Content 2Sub", - UrlSegment = "content-2sub", - Path = "/3", - Level = 1, - Url = "/content-2sub", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - return caches; - } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs new file mode 100644 index 0000000000..623472a023 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Collections.ObjectModel; +using System.Web.Routing; +using Moq; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; +using Umbraco.Core.Composing; +using Current = Umbraco.Core.Composing.Current; +using LightInject; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects.Accessors; + +namespace Umbraco.Tests.PublishedContent +{ + public abstract class PublishedContentSnapshotTestBase : PublishedContentTestBase + { + // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet + // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx + // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx + + public override void SetUp() + { + base.SetUp(); + + var umbracoContext = GetUmbracoContext(); + Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; + } + + protected override void Compose() + { + base.Compose(); + + Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); + } + + protected override TypeLoader CreatePluginManager(IServiceFactory f) + { + var pluginManager = base.CreatePluginManager(f); + + // this is so the model factory looks into the test assembly + pluginManager.AssembliesToScan = pluginManager.AssembliesToScan + .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) + .ToList(); + + return pluginManager; + } + + private UmbracoContext GetUmbracoContext() + { + RouteData routeData = null; + + var publishedSnapshot = CreatePublishedSnapshot(); + + var publishedSnapshotService = new Mock(); + publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); + + var globalSettings = TestObjects.GetGlobalSettings(); + + var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; + var umbracoContext = new UmbracoContext( + httpContext, + publishedSnapshotService.Object, + new WebSecurity(httpContext, Current.Services.UserService, globalSettings), + TestObjects.GetUmbracoSettings(), + Enumerable.Empty(), + globalSettings, + new TestVariationContextAccessor()); + + return umbracoContext; + } + + public override void TearDown() + { + base.TearDown(); + + Current.Reset(); + } + + private SolidPublishedSnapshot CreatePublishedSnapshot() + { + var dataTypeService = new TestObjects.TestDataTypeService( + new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); + + var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); + var caches = new SolidPublishedSnapshot(); + var cache = caches.InnerContentCache; + PopulateCache(factory, cache); + return caches; + } + + internal abstract void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache); + } +} diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 4f63533693..33e315ebec 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -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 _solidSourceValues = new Dictionary(); + private readonly IDictionary _solidValues = new Dictionary(); + private readonly IDictionary _solidXPathValues = new Dictionary(); + + public override object GetSourceValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetSourceValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture) ? _solidSourceValues[culture] : null; + } + + public override object GetValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetValue(culture, segment); + } + + return _solidValues.ContainsKey(culture) ? _solidValues[culture] : null; + } + + public override object GetXPathValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetXPathValue(culture, segment); + } + + return _solidXPathValues.ContainsKey(culture) ? _solidXPathValues[culture] : null; + } + + public override bool HasValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.HasValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture); + } + + public void SetSourceValue(string culture, object value) + { + _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")] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c86cf5d2d0..592527a4b6 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -122,6 +122,8 @@ + + diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index e6902c00db..b6dc9f4244 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -15,9 +15,9 @@ namespace Umbraco.Web.Models.PublishedContent { private readonly ILocalizationService _localizationService; - public PublishedValueLanguageFallback(ILocalizationService localizationService) + public PublishedValueLanguageFallback(ServiceContext services) { - _localizationService = localizationService; + _localizationService = services.LocalizationService; } /// From d1a31ad8f8383ed1a860e33c8657901405826841 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 12 Jul 2018 08:12:54 +0100 Subject: [PATCH 11/12] Removed index from fallback language column --- src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 25ca43f918..f87930269a 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -45,7 +45,6 @@ namespace Umbraco.Core.Persistence.Dtos /// [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.NonClustered)] [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } From 91a0ee2c93d0a136bad6b0ed8caf44368ea2f625 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 12 Jul 2018 20:52:02 +0100 Subject: [PATCH 12/12] Refactored to object graph reference of fallback language to just use id --- src/Umbraco.Core/Models/ILanguage.cs | 4 +- src/Umbraco.Core/Models/Language.cs | 10 ++-- .../Persistence/Factories/LanguageFactory.cs | 9 +--- .../Implement/LanguageRepository.cs | 15 +----- .../Repositories/LanguageRepositoryTest.cs | 18 +++---- .../PublishedContentLanuageVariantTests.cs | 8 ++-- .../src/views/languages/edit.controller.js | 20 -------- .../src/views/languages/edit.html | 2 +- .../views/languages/overview.controller.js | 10 ++++ .../src/views/languages/overview.html | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 47 ++++++++----------- .../Models/ContentEditing/Language.cs | 4 +- .../PublishedValueLanguageFallback.cs | 46 +++++++++--------- 13 files changed, 80 insertions(+), 115 deletions(-) diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index f02bd33d2b..8d1c092e13 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -35,9 +35,9 @@ namespace Umbraco.Core.Models bool Mandatory { get; set; } /// - /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// 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. /// - ILanguage FallbackLanguage { get; set; } + int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 42c305d492..5fcd5cd50e 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; - private ILanguage _fallbackLanguage; + private int? _fallbackLanguageId; public Language(string isoCode) { @@ -33,7 +33,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); - public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguage); + public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguageId); } /// @@ -74,10 +74,10 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } - public ILanguage FallbackLanguage + public int? FallbackLanguageId { - get => _fallbackLanguage; - set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguage, Ps.Value.FallbackLanguageSelector); + get => _fallbackLanguageId; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector); } } } diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index c805ae7f5a..7ab36d15d6 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -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,17 +16,12 @@ 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)); } - if (entity.FallbackLanguage != null) - { - dto.FallbackLanguageId = entity.FallbackLanguage.Id; - } - return dto; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 96bb088f2b..af5d28c18e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -54,7 +54,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get languages var dtos = Database.Fetch(sql); var languages = dtos.Select(ConvertFromDto).ToList(); - PopulateFallbackLanguages(dtos, languages); // initialize the code-id map lock (_codeIdMap) @@ -77,9 +76,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); var dtos = Database.Fetch(sql); - var languages = dtos.Select(ConvertFromDto).ToList(); - PopulateFallbackLanguages(dtos, languages); - return languages; + return dtos.Select(ConvertFromDto).ToList(); } #endregion @@ -203,16 +200,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return entity; } - private static void PopulateFallbackLanguages(List dtos, IList languages) - { - foreach (var dto in dtos.Where(x => x.FallbackLanguageId.HasValue)) - { - var language = languages.Single(x => x.Id == dto.Id); - // ReSharper disable once PossibleInvalidOperationException (DTOs with fallback languages have already been filtered in the loop condition) - language.FallbackLanguage = languages.Single(x => x.Id == dto.FallbackLanguageId.Value); - } - } - public ILanguage GetByIsoCode(string isoCode) { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index bda899789d..a63bf5e08d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -47,7 +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.FallbackLanguage, Is.Null); + Assert.That(language.FallbackLanguageId, Is.Null); } } @@ -63,7 +63,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = (ILanguage)new Language(au.Name) { CultureName = au.DisplayName, - FallbackLanguage = repository.Get(1) + FallbackLanguageId = 1 }; repository.Save(language); @@ -75,7 +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.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(language.FallbackLanguageId, Is.EqualTo(1)); } } @@ -193,7 +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.FallbackLanguage); + Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -215,7 +215,7 @@ 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.FallbackLanguage); + Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -232,14 +232,14 @@ namespace Umbraco.Tests.Persistence.Repositories var languageBR = new Language("pt-BR") { CultureName = "pt-BR", - FallbackLanguage = repository.Get(1) + 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.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(languageBR.FallbackLanguageId, Is.EqualTo(1)); } } @@ -284,7 +284,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = repository.Get(5); language.IsoCode = "pt-BR"; language.CultureName = "pt-BR"; - language.FallbackLanguage = repository.Get(1); + language.FallbackLanguageId = 1; repository.Save(language); @@ -294,7 +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.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(languageUpdated.FallbackLanguageId, Is.EqualTo(1)); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs index ea77310977..22eb4bd799 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -39,15 +39,15 @@ namespace Umbraco.Tests.PublishedContent { new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, new Language("fr") { Id = 2, CultureName = "French" }, - new Language("es") { Id = 3, CultureName = "Spanish" }, - new Language("it") { Id = 4, CultureName = "Italian" }, + 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" } }; - languages[2].FallbackLanguage = languages[0]; - languages[3].FallbackLanguage = languages[2]; var localizationService = Mock.Get(serviceContext.LocalizationService); localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); + localizationService.Setup(x => x.GetLanguageById(It.IsAny())) + .Returns((int id) => languages.SingleOrDefault(y => y.Id == id)); localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); } diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 79972725fc..523ef867cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -98,31 +98,11 @@ }); } - function setCultureForFallbackLanguage(lang) { - for (var i = 0; i < vm.availableLanguages.length; i++) { - if (vm.availableLanguages[i].id === lang.id) { - lang.culture = vm.availableLanguages[i].culture; - break; - } - } - } - function save() { if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; - // Handle selection of no fall-back language (should pass null) - if (!vm.language.fallbackLanguage.id) { - vm.language.fallbackLanguage = null; - } - - // We need to attach the ISO code to the fall-back language to pass - // server-side validation. - if (vm.language.fallbackLanguage) { - setCultureForFallbackLanguage(vm.language.fallbackLanguage); - } - languageResource.save(vm.language).then(function (lang) { formHelper.resetForm({ scope: $scope }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 5570b901e2..a2217a6649 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -67,7 +67,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index c8a728d3aa..a5c446dfb5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -13,6 +13,16 @@ vm.editLanguage = editLanguage; vm.deleteLanguage = deleteLanguage; + vm.getLanguageById = function(id) { + for (var i = 0; i < vm.languages.length; i++) { + if (vm.languages[i].id === id) { + return vm.languages[i]; + } + } + + return null; + }; + function init() { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index f53326a491..3b75fa62bd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -36,7 +36,7 @@ - {{vm.labels.general}} {{vm.labels.mandatory}} - {{vm.labels.fallsbackTo}}: {{language.fallbackLanguage.name}} + {{vm.labels.fallsbackTo}}: {{vm.getLanguageById(language.fallbackLanguageId).name}} x.FallbackLanguage?.Id == language.Id)) + 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)); @@ -147,20 +147,21 @@ namespace Umbraco.Web.Editors CultureName = culture.DisplayName, IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, Mandatory = language.Mandatory, + FallbackLanguageId = language.FallbackLanguageId }; - AssociateFallbackLanguage(language, newLang); Services.LocalizationService.Save(newLang); return Mapper.Map(newLang); } found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; - AssociateFallbackLanguage(language, found); + found.FallbackLanguageId = language.FallbackLanguageId; - if (DoesUpdatedFallbackLanguageCreateACircularPath(found)) + string selectedFallbackLanguageCultureName; + if (DoesUpdatedFallbackLanguageCreateACircularPath(found, out selectedFallbackLanguageCultureName)) { - ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); + ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + selectedFallbackLanguageCultureName + "' would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } @@ -168,43 +169,35 @@ namespace Umbraco.Web.Editors return Mapper.Map(found); } - private static void AssociateFallbackLanguage(Language submittedLanguage, ILanguage languageToCreateOrUpdate) + private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language, out string selectedFallbackLanguageCultureName) { - if (submittedLanguage.FallbackLanguage == null) - { - languageToCreateOrUpdate.FallbackLanguage = null; - return; - } - - var fallbackLanguageCulture = CultureInfo.GetCultureInfo(submittedLanguage.FallbackLanguage.IsoCode); - languageToCreateOrUpdate.FallbackLanguage = new Core.Models.Language(fallbackLanguageCulture.Name) - { - Id = submittedLanguage.FallbackLanguage.Id, - CultureName = fallbackLanguageCulture.DisplayName - }; - } - - private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language) - { - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { + selectedFallbackLanguageCultureName = string.Empty; return false; } var languages = Services.LocalizationService.GetAllLanguages().ToArray(); - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { - if (fallbackLanguage.Id == language.Id) + 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; } - fallbackLanguage = languages.Single(x => x.Id == fallbackLanguage.Id).FallbackLanguage; + fallbackLanguageId = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).FallbackLanguageId; } + selectedFallbackLanguageCultureName = string.Empty; return false; } + + private static ILanguage GetLanguageFromCollectionById(IEnumerable languages, int id) + { + return languages.Single(x => x.Id == id); + } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index 309e111e32..7693ee836e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "isMandatory")] public bool Mandatory { get; set; } - [DataMember(Name = "fallbackLanguage")] - public Language FallbackLanguage { get; set; } + [DataMember(Name = "fallbackLanguageId")] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index b6dc9f4244..d6e8db83f4 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -107,13 +107,6 @@ namespace Umbraco.Web.Models.PublishedContent return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } - private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) - { - return value != null && - string.IsNullOrEmpty(value.ToString()) == false && - value.Equals(defaultValue) == false; - } - private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) { if (string.IsNullOrEmpty(culture)) @@ -123,22 +116,23 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + 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; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; @@ -154,22 +148,23 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + 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; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; @@ -185,34 +180,39 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + 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; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; return false; } - private ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage) + private ILanguage GetLanguageById(int id) { - // Ensure reference to next fall-back language is loaded if it exists - fallbackLanguage = _localizationService.GetLanguageById(fallbackLanguage.Id); + return _localizationService.GetLanguageById(id); + } - return fallbackLanguage.FallbackLanguage; + private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) + { + return value != null && + string.IsNullOrEmpty(value.ToString()) == false && + value.Equals(defaultValue) == false; } } }