Fixed: Empty Sabnzbd category is now properly handled. But added UI validation to recommend adding a category.

This commit is contained in:
Taloth Saldono 2015-02-18 00:43:33 +01:00
parent a8e805fd5d
commit 6803e46782
20 changed files with 207 additions and 54 deletions

View File

@ -9,10 +9,10 @@ namespace NzbDrone.Api.DownloadClient
{
}
protected override void Validate(DownloadClientDefinition definition)
protected override void Validate(DownloadClientDefinition definition, bool includeWarnings)
{
if (!definition.Enable) return;
base.Validate(definition);
base.Validate(definition, includeWarnings);
}
}
}

View File

@ -9,10 +9,10 @@ namespace NzbDrone.Api.Indexers
{
}
protected override void Validate(IndexerDefinition definition)
protected override void Validate(IndexerDefinition definition, bool includeWarnings)
{
if (!definition.Enable) return;
base.Validate(definition);
base.Validate(definition, includeWarnings);
}
}
}

View File

@ -9,10 +9,10 @@ namespace NzbDrone.Api.Metadata
{
}
protected override void Validate(MetadataDefinition definition)
protected override void Validate(MetadataDefinition definition, bool includeWarnings)
{
if (!definition.Enable) return;
base.Validate(definition);
base.Validate(definition, includeWarnings);
}
}
}

View File

@ -9,10 +9,10 @@ namespace NzbDrone.Api.Notifications
{
}
protected override void Validate(NotificationDefinition definition)
protected override void Validate(NotificationDefinition definition, bool includeWarnings)
{
if (!definition.OnGrab && !definition.OnDownload) return;
base.Validate(definition);
base.Validate(definition, includeWarnings);
}
}
}

View File

@ -2,12 +2,14 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Nancy;
using NzbDrone.Api.ClientSchema;
using NzbDrone.Api.Extensions;
using NzbDrone.Api.Mapping;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using Omu.ValueInjecter;
namespace NzbDrone.Api
@ -72,12 +74,9 @@ namespace NzbDrone.Api
private int CreateProvider(TProviderResource providerResource)
{
var providerDefinition = GetDefinition(providerResource);
var providerDefinition = GetDefinition(providerResource, false);
if (providerDefinition.Enable)
{
Test(providerDefinition);
}
Test(providerDefinition, false);
providerDefinition = _providerFactory.Create(providerDefinition);
@ -86,12 +85,14 @@ namespace NzbDrone.Api
private void UpdateProvider(TProviderResource providerResource)
{
var providerDefinition = GetDefinition(providerResource);
var providerDefinition = GetDefinition(providerResource, false);
Test(providerDefinition, false);
_providerFactory.Update(providerDefinition);
}
private TProviderDefinition GetDefinition(TProviderResource providerResource)
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false)
{
var definition = new TProviderDefinition();
@ -105,7 +106,7 @@ namespace NzbDrone.Api
var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract);
definition.Settings = (IProviderConfig)SchemaBuilder.ReadFormSchema(providerResource.Fields, configContract, preset);
Validate(definition);
Validate(definition, includeWarnings);
return definition;
}
@ -149,31 +150,42 @@ namespace NzbDrone.Api
private Response Test(TProviderResource providerResource)
{
var providerDefinition = GetDefinition(providerResource);
var providerDefinition = GetDefinition(providerResource, true);
Test(providerDefinition);
Test(providerDefinition, true);
return "{}";
}
private void Test(TProviderDefinition providerDefinition)
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)
{
var result = _providerFactory.Test(providerDefinition);
var validationResult = definition.Settings.Validate();
VerifyValidationResult(validationResult, includeWarnings);
}
protected virtual void Test(TProviderDefinition definition, bool includeWarnings)
{
if (!definition.Enable) return;
var validationResult = _providerFactory.Test(definition);
VerifyValidationResult(validationResult, includeWarnings);
}
protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings)
{
var result = new NzbDroneValidationResult(validationResult.Errors);
if (includeWarnings && (!result.IsValid || result.HasWarnings))
{
throw new ValidationException(result.Failures);
}
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
protected virtual void Validate(TProviderDefinition definition)
{
var validationResult = definition.Settings.Validate();
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
}
}
}

View File

@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(2, Label = "Password", Type = FieldType.Password)]
public String Password { get; set; }
[FieldDefinition(3, Label = "Category", Type = FieldType.Textbox)]
[FieldDefinition(3, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public String TvCategory { get; set; }
[FieldDefinition(4, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]

View File

@ -3,6 +3,7 @@ using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
namespace NzbDrone.Core.Download.Clients.Nzbget
@ -18,6 +19,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
RuleFor(c => c.TvCategory).NotEmpty().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath));
RuleFor(c => c.TvCategoryLocalPath).IsValidPath().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath));
RuleFor(c => c.TvCategory).NotEmpty().WithMessage("A category is recommended").AsWarning();
}
}
@ -46,7 +49,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
public String Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox)]
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public String TvCategory { get; set; }
// TODO: Remove around January 2015, this setting was superceded by the RemotePathMappingService, but has to remain for a while to properly migrate.

View File

@ -181,7 +181,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
foreach (var downloadClientItem in GetQueue().Concat(GetHistory()))
{
if (downloadClientItem.Category == Settings.TvCategory)
if (downloadClientItem.Category == Settings.TvCategory || downloadClientItem.Category == "*" && Settings.TvCategory.IsNullOrWhiteSpace())
{
yield return downloadClientItem;
}

View File

@ -3,6 +3,7 @@ using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
@ -24,6 +25,10 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
RuleFor(c => c.Password).NotEmpty()
.WithMessage("Password is required when API key is not configured")
.When(c => String.IsNullOrWhiteSpace(c.ApiKey));
RuleFor(c => c.TvCategory).NotEmpty()
.WithMessage("A category is recommended")
.AsWarning();
}
}
@ -55,7 +60,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
public String Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)]
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public String TvCategory { get; set; }
// TODO: Remove around January 2015, this setting was superceded by the RemotePathMappingService, but has to remain for a while to properly migrate.

View File

@ -42,7 +42,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
public String Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)]
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional. Creates a .[category] subdirectory in the output directory.")]
public String TvCategory { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]

View File

@ -39,7 +39,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
public String Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox)]
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public String TvCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]

View File

@ -869,8 +869,10 @@
<Compile Include="Update\UpdateVerificationFailedException.cs" />
<Compile Include="Validation\FolderValidator.cs" />
<Compile Include="Validation\IpValidation.cs" />
<Compile Include="Validation\LangaugeValidator.cs" />
<Compile Include="Validation\LanguageValidator.cs" />
<Compile Include="Validation\NzbDroneValidationFailure.cs" />
<Compile Include="Validation\NzbDroneValidationResult.cs" />
<Compile Include="Validation\NzbDroneValidationState.cs" />
<Compile Include="Validation\Paths\DroneFactoryValidator.cs" />
<Compile Include="Validation\Paths\PathExistsValidator.cs" />
<Compile Include="Validation\Paths\PathValidator.cs" />

View File

@ -2,9 +2,9 @@
namespace NzbDrone.Core.Validation
{
public class LangaugeValidator : PropertyValidator
public class LanguageValidator : PropertyValidator
{
public LangaugeValidator()
public LanguageValidator()
: base("Unknown Language")
{
}

View File

@ -5,13 +5,29 @@ namespace NzbDrone.Core.Validation
{
public class NzbDroneValidationFailure : ValidationFailure
{
public String DetailedDescription { get; set; }
public String InfoLink { get; set; }
public bool IsWarning { get; set; }
public string DetailedDescription { get; set; }
public string InfoLink { get; set; }
public NzbDroneValidationFailure(String propertyName, String error)
public NzbDroneValidationFailure(string propertyName, string error)
: base(propertyName, error)
{
}
public NzbDroneValidationFailure(string propertyName, string error, object attemptedValue)
: base(propertyName, error, attemptedValue)
{
}
public NzbDroneValidationFailure(ValidationFailure validationFailure)
: base(validationFailure.PropertyName, validationFailure.ErrorMessage, validationFailure.AttemptedValue)
{
CustomState = validationFailure.CustomState;
var state = validationFailure.CustomState as NzbDroneValidationState;
IsWarning = state != null && state.IsWarning;
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentValidation;
using FluentValidation.Results;
namespace NzbDrone.Core.Validation
{
public class NzbDroneValidationResult : ValidationResult
{
public NzbDroneValidationResult()
{
}
public NzbDroneValidationResult(IEnumerable<ValidationFailure> failures)
{
var errors = new List<NzbDroneValidationFailure>();
var warnings = new List<NzbDroneValidationFailure>();
foreach (var failureBase in failures)
{
var failure = failureBase as NzbDroneValidationFailure;
if (failure == null)
{
failure = new NzbDroneValidationFailure(failureBase);
}
if (failure.IsWarning)
{
warnings.Add(failure);
}
else
{
errors.Add(failure);
}
}
Failures = errors.Concat(warnings).ToList();
Errors = errors;
errors.ForEach(base.Errors.Add);
Warnings = warnings;
}
public IList<NzbDroneValidationFailure> Failures { get; private set; }
public new IList<NzbDroneValidationFailure> Errors { get; private set; }
public IList<NzbDroneValidationFailure> Warnings { get; private set; }
public virtual bool HasWarnings
{
get { return Warnings.Any(); }
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentValidation;
using FluentValidation.Results;
namespace NzbDrone.Core.Validation
{
public class NzbDroneValidationState
{
public static NzbDroneValidationState Warning = new NzbDroneValidationState { IsWarning = true };
public bool IsWarning { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentValidation;
using FluentValidation.Results;
namespace NzbDrone.Core.Validation
{
public abstract class NzbDroneValidator<T> : AbstractValidator<T>
{
public override ValidationResult Validate(T instance)
{
return new NzbDroneValidationResult(base.Validate(instance).Errors);
}
}
}

View File

@ -35,7 +35,12 @@ namespace NzbDrone.Core.Validation
public static IRuleBuilderOptions<T, Language> ValidLanguage<T>(this IRuleBuilder<T, Language> ruleBuilder)
{
return ruleBuilder.SetValidator(new LangaugeValidator());
return ruleBuilder.SetValidator(new LanguageValidator());
}
public static IRuleBuilderOptions<T, TProp> AsWarning<T, TProp>(this IRuleBuilderOptions<T, TProp> ruleBuilder)
{
return ruleBuilder.WithState(v => NzbDroneValidationState.Warning);
}
}
}

View File

@ -102,6 +102,20 @@ h3 {
}
}
.has-warning {
.help-inline {
color: orange;
margin-left: 0px;
}
}
.validation-warning {
i {
text-decoration: none;
color: orange;
}
}
// Tooltips
.help-inline-checkbox, .help-inline {

View File

@ -37,16 +37,21 @@ module.exports = function() {
} else {
var inputGroup = formGroup.find('.input-group');
if (inputGroup.length === 0) {
formGroup.append('<span class="help-inline validation-error">' + errorMessage + '</span>');
}
var validationClass = error.isWarning ? 'validation-warning' : 'validation-error';
if (inputGroup.length === 0) {
formGroup.append('<span class="help-inline {0}">{1}</span>'.format(validationClass, errorMessage));
}
else {
inputGroup.parent().append('<span class="help-block validation-error">' + errorMessage + '</span>');
inputGroup.parent().append('<span class="help-block {0}">{1}</span>'.format(validationClass, errorMessage));
}
}
formGroup.addClass('has-error');
if (error.isWarning) {
formGroup.addClass('has-warning');
} else {
formGroup.addClass('has-error');
}
return formGroup.find('.help-inline').text();
};
@ -59,20 +64,23 @@ module.exports = function() {
var errorMessage = this.formatErrorMessage(error);
if (this.find('.modal-body')) {
this.find('.modal-body').prepend('<div class="alert alert-danger validation-error">' + errorMessage + '</div>');
var target = this.find('.modal-body');
if (!target.length) {
target = this;
}
else {
this.prepend('<div class="alert alert-danger validation-error">' + errorMessage + '</div>');
}
var validationClass = error.isWarning ? 'alert alert-warning validation-warning' : 'alert alert-danger validation-error';
target.prepend('<div class="{0}">{1}</div>'.format(validationClass, errorMessage));
};
$.fn.removeAllErrors = function() {
this.find('.has-error').removeClass('has-error');
this.find('.has-warning').removeClass('has-warning');
this.find('.error').removeClass('error');
this.find('.validation-errors').removeClass('alert').removeClass('alert-danger').html('');
this.find('.validation-errors').removeClass('alert').removeClass('alert-danger').removeClass('alert-warning').html('');
this.find('.validation-error').remove();
this.find('.validation-warning').remove();
return this.find('.help-inline.error-message').remove();
};