Segments: Only validate segment values for cultures they are defined for (closes #21029) (#21033)

* Only validate segment values for cultures they are defined for.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Integration test suppressions.

* Remove previous implementation using ISegmentService and rely on values provided in the model to determine segments with cultures.

* Omit null culture and remove passing but unrealistic tests.

* Fixed nullability error.

* Apply suggestions from code review

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Relocated function following code review.

* Reset unchanged files.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
This commit is contained in:
Andy Butland
2025-12-04 12:54:44 +01:00
committed by GitHub
parent effccef81d
commit e6c7ef8904
4 changed files with 290 additions and 18 deletions

View File

@@ -50,7 +50,7 @@ internal abstract class ContentValidationServiceBase<TContentType>
cultures = await GetCultureCodes();
}
// we don't have any managed segments, so we have to make do with the ones passed in the model
// We don't have managed segments, so we have to make do with the ones passed in the model.
var segments =
new string?[] { null }
.Union(contentEditingModelBase.Variants
@@ -105,21 +105,39 @@ internal abstract class ContentValidationServiceBase<TContentType>
}
}
foreach (IPropertyType propertyType in cultureAndSegmentVariantPropertyTypes)
if (cultureAndSegmentVariantPropertyTypes.Length > 0)
{
foreach (var culture in cultures)
{
foreach (var segment in segments.DefaultIfEmpty(null))
{
var validationContext = new PropertyValidationContext
{
Culture = culture, Segment = segment, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
};
// Get a mapping of segments to their associated cultures based on the variants and properties provided in the model.
// Without managed segments again we need to rely on the model data.
Dictionary<string, HashSet<string>> segmentCultures = GetPopulatedSegmentCultures(contentEditingModelBase, cultures);
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture) && propertyValue.Segment.InvariantEquals(segment));
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
foreach (IPropertyType propertyType in cultureAndSegmentVariantPropertyTypes)
{
foreach (var culture in cultures)
{
foreach (var segment in segments.DefaultIfEmpty(null))
{
// Skip validation if the segment has cultures defined and the current culture is not included.
if (segment is not null &&
segmentCultures.TryGetValue(segment, out HashSet<string>? associatedCultures) &&
associatedCultures.Contains(culture) is false)
{
continue;
}
var validationContext = new PropertyValidationContext
{
Culture = culture,
Segment = segment,
CulturesBeingValidated = cultures,
SegmentsBeingValidated = segments,
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture) && propertyValue.Segment.InvariantEquals(segment));
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
}
}
}
@@ -140,6 +158,31 @@ internal abstract class ContentValidationServiceBase<TContentType>
private async Task<string[]> GetCultureCodes() => (await _languageService.GetAllIsoCodesAsync()).ToArray();
/// <summary>
/// Gets a dictionary of segments along with the cultures they are associated with.
/// </summary>
/// <param name="contentEditingModel">The content editing model.</param>
/// <param name="cultures">The cultures to consider when finding associated cultures for each segment.</param>
/// <returns>
/// A dictionary where the key is a unique segment from <see cref="ContentEditingModelBase.Variants"/> and the value is
/// the set of cultures that have at least one property defined for that segment in <see cref="ContentEditingModelBase.Properties"/>.
/// </returns>
/// <remarks>
/// Internal to support unit testing.
/// </remarks>
internal static Dictionary<string, HashSet<string>> GetPopulatedSegmentCultures(ContentEditingModelBase contentEditingModel, string[] cultures)
{
IEnumerable<string> uniqueSegments = contentEditingModel.Variants.Select(variant => variant.Segment).WhereNotNull().Distinct();
return uniqueSegments.ToDictionary(
segment => segment,
segment => contentEditingModel.Properties
.Where(property => property.Segment.InvariantEquals(segment))
.Where(property => property.Culture is not null && cultures.Contains(property.Culture))
.Select(property => property.Culture!)
.ToHashSet());
}
private IEnumerable<PropertyValidationError> ValidateProperty(IPropertyType propertyType, PropertyValueModel? propertyValueModel, PropertyValidationContext validationContext)
{
ValidationResult[] validationResults = _propertyValidationService