diff --git a/NzbDrone.Web/Helpers/Validation/RequiredIfAnyAttribute.cs b/NzbDrone.Web/Helpers/Validation/RequiredIfAnyAttribute.cs new file mode 100644 index 000000000..e0ccf3ce6 --- /dev/null +++ b/NzbDrone.Web/Helpers/Validation/RequiredIfAnyAttribute.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Web; +using System.Web.Mvc; + +namespace NzbDrone.Web.Helpers.Validation +{ + public class RequiredIfAnyAttribute : ValidationAttribute, IClientValidatable + { + private RequiredAttribute _innerAttribute = new RequiredAttribute(); + + public string[] DependentProperties { get; set; } + public object[] TargetValues { get; set; } + + public RequiredIfAnyAttribute(string[] dependentProperties, params object[] targetValues) + { + if (dependentProperties.Count() != targetValues.Count()) + throw new ArgumentException("Dependent properties count must equal values count"); + + this.DependentProperties = dependentProperties; + this.TargetValues = targetValues; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + for (int i = 0; i < DependentProperties.Count(); i++) + { + // get a reference to the property this validation depends upon + var containerType = validationContext.ObjectInstance.GetType(); + var field = containerType.GetProperty(this.DependentProperties[i]); + + if (field != null) + { + // get the value of the dependent property + var dependentvalue = field.GetValue(validationContext.ObjectInstance, null); + + // compare the value against the target value + if ((dependentvalue == null && this.TargetValues[i] == null) || + (dependentvalue != null && dependentvalue.Equals(this.TargetValues[i]))) + { + // match => means we should try validating this field + if (!_innerAttribute.IsValid(value)) + // validation failed - return an error + return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName }); + } + } + } + + return ValidationResult.Success; + } + + public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context) + { + var rule = new ModelClientValidationRule() + { + ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()), + ValidationType = "requiredifany", + }; + + var properties = new List(); + var values = new List(); + + for (int i = 0; i < DependentProperties.Count(); i++) + { + string depProp = BuildDependentPropertyId(metadata, context as ViewContext, DependentProperties[i]); + + // find the value on the control we depend on; + // if it's a bool, format it javascript style + // (the default is True or False!) + string targetValue = (TargetValues[i] ?? "").ToString(); + if (TargetValues[i].GetType() == typeof(bool)) + targetValue = targetValue.ToLower(); + + properties.Add(depProp); + values.Add(targetValue); + } + + rule.ValidationParameters.Add("dependentproperties", String.Join("|", properties)); + rule.ValidationParameters.Add("targetvalues", String.Join("|", values)); + + yield return rule; + } + + private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext, string dependentProperty) + { + // build the ID of the property + string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(dependentProperty); + // unfortunately this will have the name of the current field appended to the beginning, + // because the TemplateInfo's context has had this fieldname appended to it. Instead, we + // want to get the context as though it was one level higher (i.e. outside the current property, + // which is the containing object (our Person), and hence the same level as the dependent property. + var thisField = metadata.PropertyName + "_"; + if (depProp.StartsWith(thisField)) + // strip it off again + depProp = depProp.Substring(thisField.Length); + return depProp; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Web/Models/NotificationSettingsModel.cs b/NzbDrone.Web/Models/NotificationSettingsModel.cs index d9596e2b3..02e7d2540 100644 --- a/NzbDrone.Web/Models/NotificationSettingsModel.cs +++ b/NzbDrone.Web/Models/NotificationSettingsModel.cs @@ -201,7 +201,7 @@ namespace NzbDrone.Web.Models [DataType(DataType.Text)] [DisplayName("Client Hosts")] [Description("Plex client hosts with port, comma separated for multiple clients")] - [RequiredIf("PlexNotifyOnGrab", true, ErrorMessage = "Required when Plex Notifications are enabled")] + [RequiredIfAny(new string[]{ "PlexNotifyOnGrab", "PlexNotifyOnDownload" }, new object[]{ true, true }, ErrorMessage = "Required when Plex Notifications are enabled")] [DisplayFormat(ConvertEmptyStringToNull = false)] public string PlexClientHosts { get; set; } diff --git a/NzbDrone.Web/NzbDrone.Web.csproj b/NzbDrone.Web/NzbDrone.Web.csproj index af22938c2..776a3069d 100644 --- a/NzbDrone.Web/NzbDrone.Web.csproj +++ b/NzbDrone.Web/NzbDrone.Web.csproj @@ -131,6 +131,7 @@ + diff --git a/NzbDrone.Web/Scripts/conditional-validation.js b/NzbDrone.Web/Scripts/conditional-validation.js index 6c6421726..5797cdce9 100644 --- a/NzbDrone.Web/Scripts/conditional-validation.js +++ b/NzbDrone.Web/Scripts/conditional-validation.js @@ -40,4 +40,54 @@ $.validator.unobtrusive.adapters.add( targetvalue: options.params['targetvalue'] }; options.messages['requiredif'] = options.message; - }); +}); + + +$.validator.addMethod('requiredifany', + function (value, element, parameters) { + + console.log(parameters['dependentproperties']); + console.log(parameters['targetvalues']); + + var dependentProperties = parameters['dependentproperties'].split('|'); + var targetValues = parameters['targetvalues'].split('|'); + + for (var i = 0; i < dependentProperties.length; i++) { + var id = '#' + dependentProperties[i]; + + // get the target value (as a string, + // as that's what actual value will be) + var targetvalue = targetValues[i]; + targetvalue = + (targetvalue == null ? '' : targetvalue).toString(); + + // get the actual value of the target control + // note - this probably needs to cater for more + // control types, e.g. radios + var control = $(id); + var controltype = control.attr('type'); + var actualvalue = + controltype === 'checkbox' ? + (control.attr('checked') == "checked" ? "true" : "false") : + control.val(); + + // if the condition is true, reuse the existing + // required field validator functionality + if (targetvalue === actualvalue) + return $.validator.methods.required.call( + this, value, element, parameters); + } + return true; + } +); + + $.validator.unobtrusive.adapters.add( + 'requiredifany', + ['dependentproperties', 'targetvalues'], + function (options) { + options.rules['requiredifany'] = { + dependentproperties: options.params['dependentproperties'], + targetvalues: options.params['targetvalues'] + }; + options.messages['requiredifany'] = options.message; + }); \ No newline at end of file