Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 44 additions & 18 deletions src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;

Check notice on line 1 in src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (release/17.0)

✅ Getting better: Code Duplication

reduced similar code in: TryGetValueWithDefaultLanguageFallback,TryGetValueWithDefaultLanguageFallback. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

Check notice on line 1 in src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (release/17.0)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 5.06 to 4.61, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.

Check notice on line 1 in src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (release/17.0)

✅ Getting better: Missing Arguments Abstractions

The average number of function arguments decreases from 5.19 to 5.00, threshold = 4.00. The functions in this file have too many arguments, indicating a lack of encapsulation or too many responsibilities in the same functions. Avoid adding more.
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
Expand Down Expand Up @@ -317,7 +317,7 @@
}

var culture2 = language2.IsoCode;
T? culture2Value = getValue(culture2, segment);
T? culture2Value = TryGetExplicitlyContextualizedValue(getValue, culture2, segment);
if (culture2Value != null)
{
value = culture2Value;
Expand All @@ -329,6 +329,26 @@
}

private bool TryGetValueWithDefaultLanguageFallback<T>(IPublishedProperty property, string? culture, string? segment, out T? value)
=> TryGetValueWithDefaultLanguageFallback(
(actualCulture, actualSegment)
=> property.HasValue(actualCulture, actualSegment)
? property.Value<T>(this, actualCulture, actualSegment)
: default,
culture,
segment,
out value);

private bool TryGetValueWithDefaultLanguageFallback<T>(IPublishedElement element, string alias, string? culture, string? segment, out T? value)
=> TryGetValueWithDefaultLanguageFallback(
(actualCulture, actualSegment)
=> element.HasValue(alias, actualCulture, actualSegment)
? element.Value<T>(this, alias, actualCulture, actualSegment)
: default,
culture,
segment,
out value);

private bool TryGetValueWithDefaultLanguageFallback<T>(TryGetValueForCultureAndSegment<T> getValue, string? culture, string? segment, out T? value)
{
value = default;

Expand All @@ -337,33 +357,39 @@
return false;
}

string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode();
if (culture.InvariantEquals(defaultCulture) == false && property.HasValue(defaultCulture, segment))
var defaultCulture = _localizationService?.GetDefaultLanguageIsoCode();
if (defaultCulture.IsNullOrWhiteSpace())
{
value = property.Value<T>(this, defaultCulture, segment);
return true;
return false;
}

return false;
}

private bool TryGetValueWithDefaultLanguageFallback<T>(IPublishedElement element, string alias, string? culture, string? segment, out T? value)
{
value = default;

if (culture.IsNullOrWhiteSpace())
if (culture.InvariantEquals(defaultCulture))
{
return false;
}

string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode();
if (culture.InvariantEquals(defaultCulture) == false && element.HasValue(alias, defaultCulture, segment))
T? fallbackValue = TryGetExplicitlyContextualizedValue(getValue, defaultCulture, segment);
if (fallbackValue == null)
{
value = element.Value<T>(this, alias, defaultCulture, segment);
return true;
return false;
}

return false;
value = fallbackValue;
return true;
}

private T? TryGetExplicitlyContextualizedValue<T>(TryGetValueForCultureAndSegment<T> getValue, string culture, string? segment)
{
VariationContext? current = _variationContextAccessor.VariationContext;
try
{
_variationContextAccessor.VariationContext = new VariationContext(culture, segment);
return getValue(culture, segment);
}
finally
{
_variationContextAccessor.VariationContext = current;
}
}

private delegate T? TryGetValueForCultureAndSegment<out T>(string actualCulture, string? actualSegment);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;

Check warning on line 1 in tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (release/17.0)

❌ New issue: Primitive Obsession

In this module, 92.9% of all function arguments are primitive types, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.

Check warning on line 1 in tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (release/17.0)

❌ New issue: String Heavy Function Arguments

In this module, 78.6% of all arguments to its 8 functions are strings. The threshold for string arguments is 39.0%. The functions in this file have a high ratio of strings as arguments. Avoid adding more.
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
Expand Down Expand Up @@ -35,6 +35,8 @@

private IApiContentBuilder ApiContentBuilder => GetRequiredService<IApiContentBuilder>();

private ILanguageService LanguageService => GetRequiredService<ILanguageService>();

protected override void CustomTestSetup(IUmbracoBuilder builder)
=> builder
.AddUmbracoHybridCache()
Expand Down Expand Up @@ -98,6 +100,36 @@
Assert.AreEqual(invariantTitle, invariantValue);
}

[TestCase("Danish title", true)]
[TestCase("Danish title", false)]
[TestCase(null, true)]
[TestCase(null, false)]
public async Task Property_Value_Can_Perform_Explicit_Language_Fallback(string? danishTitle, bool performFallbackToDefaultLanguage)
{
var danishLanguage = new Language("da-DK", "Danish")
{
FallbackIsoCode = "en-US"
};
await LanguageService.CreateAsync(danishLanguage, Constants.Security.SuperUserKey);

UmbracoContextFactory.EnsureUmbracoContext();

const string englishTitle = "English title";
var publishedContent = await SetupCultureVariantContentAsync(englishTitle, danishTitle);

VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK", segment: null);
var danishValue = publishedContent.Value<string>(PublishedValueFallback, "title");
Assert.AreEqual(danishTitle ?? string.Empty, danishValue);

var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage;
var fallbackValue = publishedContent.Value<string>(PublishedValueFallback, "title", fallback: fallback);
Assert.AreEqual(danishTitle ?? englishTitle, fallbackValue);

VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US", segment: null);
var englishValue = publishedContent.Value<string>(PublishedValueFallback, "title");
Assert.AreEqual(englishTitle, englishValue);
}

private async Task<IPublishedContent> SetupSegmentedContentAsync(string? invariantTitle, string? segmentedTitle)
{
var contentType = new ContentTypeBuilder()
Expand All @@ -124,11 +156,47 @@
ContentService.Save(content);
ContentService.Publish(content, ["*"]);

return GetPublishedContent(content.Key);
}

Check warning on line 160 in tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (release/17.0)

❌ New issue: Code Duplication

The module contains 2 functions with similar structure: SetupCultureVariantContentAsync,SetupSegmentedContentAsync. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

private async Task<IPublishedContent> SetupCultureVariantContentAsync(string englishTitle, string? danishTitle)
{
var contentType = new ContentTypeBuilder()
.WithAlias("theContentType")
.WithContentVariation(ContentVariation.Culture)
.AddPropertyType()
.WithAlias("title")
.WithName("Title")
.WithDataTypeId(Constants.DataTypes.Textbox)
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
.WithValueStorageType(ValueStorageType.Nvarchar)
.WithVariations(ContentVariation.Culture)
.Done()
.WithAllowAsRoot(true)
.Build();
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);

var content = new ContentBuilder()
.WithContentType(contentType)
.WithCultureName("en-US", "EN")
.WithCultureName("da-DK", "DA")
.WithName("Content")
.Build();
content.SetValue("title", englishTitle, culture: "en-US");
content.SetValue("title", danishTitle, culture: "da-DK");
ContentService.Save(content);
ContentService.Publish(content, ["en-US", "da-DK"]);

return GetPublishedContent(content.Key);
}

private IPublishedContent GetPublishedContent(Guid key)
{
ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]);

UmbracoContextAccessor.Clear();
var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext;
var publishedContent = umbracoContext.Content.GetById(content.Key);
var publishedContent = umbracoContext.Content.GetById(key);
Assert.IsNotNull(publishedContent);

return publishedContent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1932,4 +1932,95 @@ void AssertPropertyValues(string culture, IContent expectedPickedContent)
Assert.AreEqual(expectedPickedContent.Key, actualPickedPublishedContent.Key);
}
}

[TestCase(ContentVariation.Culture, false)]
[TestCase(ContentVariation.Culture, true)]
[TestCase(ContentVariation.Nothing, false)]
[TestCase(ContentVariation.Nothing, true)]
public async Task Can_Perform_Language_Fallback(ContentVariation elementTypeVariation, bool performFallbackToDefaultLanguage)
{
var daDkLanguage = await LanguageService.GetAsync("da-DK");
Assert.IsNotNull(daDkLanguage);
daDkLanguage.FallbackIsoCode = "en-US";
var saveLanguageResult = await LanguageService.UpdateAsync(daDkLanguage, Constants.Security.SuperUserKey);
Assert.IsTrue(saveLanguageResult.Success);

daDkLanguage = await LanguageService.GetAsync("da-DK");
Assert.AreEqual("en-US", daDkLanguage?.FallbackIsoCode);

var elementType = CreateElementType(elementTypeVariation);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture);

var content = CreateContent(
contentType,
elementType,
new []
{
new BlockProperty(
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "English invariantText content value" },
new() { Alias = "variantText", Value = "English variantText content value" }
},
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "English invariantText settings value" },
new() { Alias = "variantText", Value = "English variantText settings value" }
},
"en-US",
null)
},
true);

AssertPropertyValuesWithFallback("en-US",
"English invariantText content value", "English variantText content value",
"English invariantText settings value", "English variantText settings value");

AssetEmptyPropertyValues("da-DK");

AssertPropertyValuesWithFallback("da-DK",
"English invariantText content value", "English variantText content value",
"English invariantText settings value", "English variantText settings value");

void AssertPropertyValuesWithFallback(string culture,
string expectedInvariantContentValue, string expectedVariantContentValue,
string expectedInvariantSettingsValue, string expectedVariantSettingsValue)
{
SetVariationContext(culture, null);
var publishedContent = GetPublishedContent(content.Key);

var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage;

var publishedValueFallback = GetRequiredService<IPublishedValueFallback>();
var value = publishedContent.Value<BlockListModel>(publishedValueFallback, "blocks", fallback: fallback);
Assert.IsNotNull(value);
Assert.AreEqual(1, value.Count);

var blockListItem = value.First();
Assert.AreEqual(2, blockListItem.Content.Properties.Count());
Assert.Multiple(() =>
{
Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value<string>("invariantText"));
Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value<string>("variantText"));
});

Assert.AreEqual(2, blockListItem.Settings.Properties.Count());
Assert.Multiple(() =>
{
Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value<string>("invariantText"));
Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value<string>("variantText"));
});
}

void AssetEmptyPropertyValues(string culture)
{
SetVariationContext(culture, null);
var publishedContent = GetPublishedContent(content.Key);

var value = publishedContent.Value<BlockListModel>("blocks");
Assert.NotNull(value);
Assert.IsEmpty(value);
}
}
}