diff --git a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs b/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs index 4150fe8c26..9fc3cee4ef 100644 --- a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs +++ b/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs @@ -12,6 +12,31 @@ namespace Umbraco.Tests.Web public class ModelStateExtensionsTests { + [Test] + public void Get_Cultures_With_Errors() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property + ms.AddPropertyError(new ValidationResult("title missing"), "title", "en-US"); //variant property + + var result = ms.GetCulturesWithErrors(localizationService.Object); + + //even though there are 2 errors, they are both for en-US since that is the default language and one of the errors is for an invariant property + Assert.AreEqual(1, result.Count); + Assert.AreEqual("en-US", result[0]); + + ms = new ModelStateDictionary(); + ms.AddCultureValidationError("en-US", "generic culture error"); + + result = ms.GetCulturesWithErrors(localizationService.Object); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("en-US", result[0]); + } + [Test] public void Get_Cultures_With_Property_Errors() { 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 a1bb50e721..b9561a8d70 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1242,6 +1242,7 @@ To manage your website, simply open the Umbraco back office and start adding con %0% can not be published, because a parent page is not published. ]]> + Validation failed for required language '%0%'. The language was saved but not published. Publishing in progress - please wait... %0% out of %1% pages have been published... %0% has been published @@ -1409,7 +1410,7 @@ To manage your website, simply open the Umbraco back office and start adding con User %0% was deleted Invite user Invitation has been re-sent to %0% - Cannot publish the document since the required '%0%' is not published + Cannot publish the document since the required '%0%' is not published Validation failed for language '%0%' Document type was exported to file An error occurred while exporting the document type diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 08f64a9d7b..b6d0e2a9ff 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -689,7 +689,7 @@ namespace Umbraco.Web.Editors { if (variantCount > 1) { - var cultureErrors = ModelState.GetCulturesWithPropertyErrors(Services.LocalizationService); + var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService); foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray()) { AddSuccessNotification(notifications, c, @@ -882,7 +882,7 @@ namespace Umbraco.Web.Editors { if (variantCount > 1) { - var cultureErrors = ModelState.GetCulturesWithPropertyErrors(Services.LocalizationService); + var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService); foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray()) { AddSuccessNotification(notifications, c, @@ -1142,16 +1142,19 @@ namespace Umbraco.Web.Editors var mandatoryCultures = _allLangs.Value.Values.Where(x => x.IsMandatory).Select(x => x.IsoCode).ToList(); + var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService); + //validate if we can publish based on the mandatory language requirements var canPublish = ValidatePublishingMandatoryLanguages( - contentItem, cultureVariants, mandatoryCultures, "speechBubbles/contentReqCulturePublishError", - mandatoryVariant => mandatoryVariant.Publish, out var _); + cultureErrors, + contentItem, cultureVariants, mandatoryCultures, + mandatoryVariant => mandatoryVariant.Publish); //Now check if there are validation errors on each variant. //If validation errors are detected on a variant and it's state is set to 'publish', then we //need to change it to 'save'. //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. - var cultureErrors = ModelState.GetCulturesWithPropertyErrors(Services.LocalizationService); + foreach (var variant in contentItem.Variants) { if (cultureErrors.Contains(variant.Culture)) @@ -1211,16 +1214,21 @@ namespace Umbraco.Web.Editors var mandatoryCultures = _allLangs.Value.Values.Where(x => x.IsMandatory).Select(x => x.IsoCode).ToList(); - //validate if we can publish based on the mandatory language requirements + var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService); + + //validate if we can publish based on the mandatory languages selected var canPublish = ValidatePublishingMandatoryLanguages( - contentItem, cultureVariants, mandatoryCultures, "speechBubbles/contentReqCulturePublishError", - mandatoryVariant => mandatoryVariant.Publish, out var _); + cultureErrors, + contentItem, cultureVariants, mandatoryCultures, + mandatoryVariant => mandatoryVariant.Publish); + + //if none are published and there are validation errors for mandatory cultures, then we can't publish anything + //Now check if there are validation errors on each variant. //If validation errors are detected on a variant and it's state is set to 'publish', then we //need to change it to 'save'. - //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. - var cultureErrors = ModelState.GetCulturesWithPropertyErrors(Services.LocalizationService); + //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. foreach (var variant in contentItem.Variants) { if (cultureErrors.Contains(variant.Culture)) @@ -1260,23 +1268,21 @@ namespace Umbraco.Web.Editors /// /// Validate if publishing is possible based on the mandatory language requirements /// + /// /// /// /// - /// /// - /// /// private bool ValidatePublishingMandatoryLanguages( + IReadOnlyCollection culturesWithValidationErrors, ContentItemSave contentItem, IReadOnlyCollection cultureVariants, IReadOnlyList mandatoryCultures, - string localizationKey, - Func publishingCheck, - out IReadOnlyList<(ContentVariantSave mandatoryVariant, bool isPublished)> mandatoryVariants) + Func publishingCheck) { var canPublish = true; - var result = new List<(ContentVariantSave, bool)>(); + var result = new List<(ContentVariantSave model, bool publishing, bool isValid)>(); foreach (var culture in mandatoryCultures) { @@ -1285,18 +1291,39 @@ namespace Umbraco.Web.Editors var mandatoryVariant = cultureVariants.First(x => x.Culture.InvariantEquals(culture)); var isPublished = contentItem.PersistedContent.Published && contentItem.PersistedContent.IsCulturePublished(culture); - result.Add((mandatoryVariant, isPublished)); - var isPublishing = isPublished || publishingCheck(mandatoryVariant); + var isValid = !culturesWithValidationErrors.InvariantContains(culture); - if (isPublished || isPublishing) continue; - - //cannot continue publishing since a required language that is not currently being published isn't published - AddCultureValidationError(culture, localizationKey); - canPublish = false; + result.Add((mandatoryVariant, isPublished || isPublishing, isValid)); + } + + //iterate over the results by invalid first + string firstInvalidMandatoryCulture = null; + foreach (var r in result.OrderBy(x => x.isValid)) + { + if (!r.isValid) + firstInvalidMandatoryCulture = r.model.Culture; + + if (r.publishing && !r.isValid) + { + //flagged for publishing but the mandatory culture is invalid + AddCultureValidationError(r.model.Culture, "publish/contentPublishedFailedReqCultureValidationError"); + canPublish = false; + } + else if (r.publishing && r.isValid && firstInvalidMandatoryCulture != null) + { + //in this case this culture also cannot be published because another mandatory culture is invalid + AddCultureValidationError(r.model.Culture, "publish/contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture); + canPublish = false; + } + else if (!r.publishing) + { + //cannot continue publishing since a required culture that is not currently being published isn't published + AddCultureValidationError(r.model.Culture, "speechBubbles/contentReqCulturePublishError"); + canPublish = false; + } } - mandatoryVariants = result; return canPublish; } @@ -1328,14 +1355,15 @@ namespace Umbraco.Web.Editors /// /// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs /// - /// + /// Culture to assign the error to /// - private void AddCultureValidationError(string culture, string localizationKey) + /// + /// The culture used in the localization message, null by default which means will be used. + /// + private void AddCultureValidationError(string culture, string localizationKey, string cultureToken = null) { - var key = "_content_variant_" + culture + "_"; - if (ModelState.ContainsKey(key)) return; - var errMsg = Services.TextService.Localize(localizationKey, new[] { _allLangs.Value[culture].CultureName }); - ModelState.AddModelError(key, errMsg); + var errMsg = Services.TextService.Localize(localizationKey, new[] { cultureToken == null ? _allLangs.Value[culture].CultureName : _allLangs.Value[cultureToken].CultureName }); + ModelState.AddCultureValidationError(culture, errMsg); } /// @@ -1763,7 +1791,7 @@ namespace Umbraco.Web.Editors if (!ModelState.IsValid && display.Variants.Count() > 1) { //Add any culture specific errors here - var cultureErrors = ModelState.GetCulturesWithPropertyErrors(Services.LocalizationService); + var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService); foreach (var cultureError in cultureErrors) { diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index fe00538135..46be12cae8 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -58,7 +58,21 @@ namespace Umbraco.Web } /// - /// Returns a list of cultures that have property errors + /// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs + /// + /// + /// + /// + internal static void AddCultureValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, + string culture, string errMsg) + { + var key = "_content_variant_" + culture + "_"; + if (modelState.ContainsKey(key)) return; + modelState.AddModelError(key, errMsg); + } + + /// + /// Returns a list of cultures that have property validation errors errors /// /// /// @@ -81,6 +95,30 @@ namespace Umbraco.Web return cultureErrors; } + /// + /// Returns a list of cultures that have any validation errors + /// + /// + /// + /// + internal static IReadOnlyList GetCulturesWithErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, + ILocalizationService localizationService) + { + var propertyCultureErrors = modelState.GetCulturesWithPropertyErrors(localizationService); + + //now check the other special culture errors that are + var genericCultureErrors = modelState.Keys + .Where(x => x.StartsWith("_content_variant_") && x.EndsWith("_")) + .Select(x => x.TrimStart("_content_variant_").TrimEnd("_")) + .Where(x => !x.IsNullOrWhiteSpace()) + //if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language + //so errors for those must show up under the default lang. + .Select(x => x == "invariant" ? localizationService.GetDefaultLanguageIsoCode() : x) + .Distinct(); + + return propertyCultureErrors.Union(genericCultureErrors).ToList(); + } + /// /// Adds the error to model state correctly for a property so we can use it on the client side. ///