Release restrictions

New: Required terms assignable to series via tags
New: Ignored terms assignable to series via tagss
This commit is contained in:
Mark McDowall
2014-10-27 21:37:35 -07:00
parent d6ed475c63
commit 53c2962d2a
38 changed files with 794 additions and 185 deletions

View File

@@ -9,12 +9,14 @@ define(
], function ($, _, TagCollection, TagModel) {
var originalAdd = $.fn.tagsinput.Constructor.prototype.add;
var originalRemove = $.fn.tagsinput.Constructor.prototype.remove;
var originalBuild = $.fn.tagsinput.Constructor.prototype.build;
$.fn.tagsinput.Constructor.prototype.add = function (item, dontPushVal) {
var self = this;
var tagLimitations = new RegExp('[^-_a-z0-9]', 'i');
if (typeof item === 'string') {
if (typeof item === 'string' && this.options.tag) {
if (item === null || item === '' || tagLimitations.test(item)) {
return;
@@ -42,6 +44,34 @@ define(
}
};
$.fn.tagsinput.Constructor.prototype.remove = function (item, dontPushVal) {
if (item === null) {
return;
}
originalRemove.call(this, item, dontPushVal);
};
$.fn.tagsinput.Constructor.prototype.build = function (options) {
var self = this;
var defaults = {
confirmKeys : [9, 13, 32, 44, 59] //tab, enter, space, comma, semi-colon
};
options = $.extend({}, defaults, options);
self.$input.on('keydown', function (event) {
if (event.which === 9) {
var e = $.Event('keypress');
e.which = 9;
self.$input.trigger(e);
event.preventDefault();
}
});
originalBuild.call(this, options);
};
$.fn.tagInput = function (options) {
var input = this;
var model = options.model;
@@ -49,10 +79,11 @@ define(
var tags = getExistingTags(model.get(property));
var tagInput = $(this).tagsinput({
freeInput: true,
itemValue : 'id',
itemText : 'label',
trimValue : true,
tag : true,
freeInput : true,
itemValue : 'id',
itemText : 'label',
trimValue : true,
typeaheadjs : {
name: 'tags',
displayKey: 'label',

View File

@@ -24,7 +24,6 @@ define(
'click .x-remove': '_removeSeries'
},
initialize: function () {
this.model.set('profiles', Profiles);
},

View File

@@ -2,8 +2,8 @@
<legend>Remote Path Mappings</legend>
<div class="col-md-12">
<div id="remotepath-mapping-list">
<div class="remotepath-header x-header hidden-xs">
<div class="rule-setting-list">
<div class="rule-setting-header x-header hidden-xs">
<div class="row">
<span class="col-sm-2">Host</span>
<span class="col-sm-5">Remote Path</span>
@@ -12,9 +12,9 @@
</div>
<div class="rows x-rows">
</div>
<div class="remotepath-footer">
<div class="rule-setting-footer">
<div class="pull-right">
<span class="add-remotepath-mapping">
<span class="add-rule-setting-mapping">
<i class="icon-nd-add x-add" title="Add new mapping" />
</span>
</div>

View File

@@ -31,30 +31,3 @@
width: 33%;
}
}
.add-remotepath-mapping {
cursor: pointer;
font-size: 14px;
text-align: center;
display: inline-block;
padding: 2px 6px;
i {
cursor: pointer;
}
}
#remotepath-mapping-list {
.remotepath-header .row {
font-weight: bold;
line-height: 40px;
}
.rows .row {
line-height : 30px;
border-top : 1px solid #ddd;
vertical-align : middle;
padding : 5px;
}
}

View File

@@ -4,25 +4,32 @@ define([
'marionette',
'Settings/Indexers/IndexerCollection',
'Settings/Indexers/IndexerCollectionView',
'Settings/Indexers/Options/IndexerOptionsView'
], function (Marionette, IndexerCollection, CollectionView, OptionsView) {
'Settings/Indexers/Options/IndexerOptionsView',
'Settings/Indexers/Restriction/RestrictionCollection',
'Settings/Indexers/Restriction/RestrictionCollectionView'
], function (Marionette, IndexerCollection, CollectionView, OptionsView, RestrictionCollection, RestrictionCollectionView) {
return Marionette.Layout.extend({
template: 'Settings/Indexers/IndexerLayoutTemplate',
regions: {
indexers : '#x-indexers-region',
indexerOptions : '#x-indexer-options-region'
indexers : '#x-indexers-region',
indexerOptions : '#x-indexer-options-region',
restriction : '#x-restriction-region'
},
initialize: function (options) {
initialize: function () {
this.indexersCollection = new IndexerCollection();
this.indexersCollection.fetch();
this.restrictionCollection = new RestrictionCollection();
this.restrictionCollection.fetch();
},
onShow: function () {
this.indexers.show(new CollectionView({ collection: this.indexersCollection }));
this.indexerOptions.show(new OptionsView({ model: this.model }));
this.restriction.show(new RestrictionCollectionView({ collection: this.restrictionCollection }));
}
});
});

View File

@@ -1,4 +1,5 @@
<div id="x-indexers-region"></div>
<div class="form-horizontal">
<div id="x-indexer-options-region"></div>
<div id="x-restriction-region"></div>
</div>

View File

@@ -21,17 +21,4 @@
<input type="number" name="rssSyncInterval" class="form-control"/>
</div>
</div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Release Restrictions</label>
<div class="col-sm-1 col-sm-push-4 help-inline help-inline-text-area">
<i class="icon-nd-form-info" title="Blacklist NZBs based on these words (case-insensitive)"/>
</div>
<div class="col-sm-4 col-sm-pull-1">
<textarea rows="3" name="releaseRestrictions" class="form-control release-restrictions"></textarea>
<div class="text-area-help">Newline-delimited set of rules</div>
</div>
</div>
</fieldset>

View File

@@ -0,0 +1,11 @@
'use strict';
define([
'backbone',
'Settings/Indexers/Restriction/RestrictionModel'
], function (Backbone, RestrictionModel) {
return Backbone.Collection.extend({
model : RestrictionModel,
url : window.NzbDrone.ApiRoot + '/Restriction'
});
});

View File

@@ -0,0 +1,27 @@
'use strict';
define([
'AppLayout',
'marionette',
'Settings/Indexers/Restriction/RestrictionItemView',
'Settings/Indexers/Restriction/RestrictionEditView',
'Tags/TagHelpers',
'bootstrap'
], function (AppLayout, Marionette, RestrictionItemView, EditView) {
return Marionette.CompositeView.extend({
template : 'Settings/Indexers/Restriction/RestrictionCollectionViewTemplate',
itemViewContainer : '.x-rows',
itemView : RestrictionItemView,
events: {
'click .x-add' : '_addMapping'
},
_addMapping: function() {
var model = this.collection.create({ tags: [] });
var view = new EditView({ model: model, targetCollection: this.collection});
AppLayout.modalRegion.show(view);
}
});
});

View File

@@ -0,0 +1,24 @@
<fieldset class="advanced-setting">
<legend>Restrictions</legend>
<div class="col-md-12">
<div class="rule-setting-list">
<div class="rule-setting-header x-header hidden-xs">
<div class="row">
<span class="col-sm-4">Must Contain</span>
<span class="col-sm-4">Must Not Contain</span>
<span class="col-sm-3">Tags</span>
</div>
</div>
<div class="rows x-rows">
</div>
<div class="rule-setting-footer">
<div class="pull-right">
<span class="add-rule-setting-mapping">
<i class="icon-nd-add x-add" title="Add new restriction" />
</span>
</div>
</div>
</div>
</div>
</fieldset>

View File

@@ -0,0 +1,23 @@
'use strict';
define([
'vent',
'marionette'
], function (vent, Marionette) {
return Marionette.ItemView.extend({
template: 'Settings/Indexers/Restriction/RestrictionDeleteViewTemplate',
events: {
'click .x-confirm-delete': '_delete'
},
_delete: function () {
this.model.destroy({
wait : true,
success: function () {
vent.trigger(vent.Commands.CloseModalCommand);
}
});
}
});
});

View File

@@ -0,0 +1,13 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Delete Restriction</h3>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the restriction for '{{localPath}}'?</p>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">cancel</button>
<button class="btn btn-danger x-confirm-delete">delete</button>
</div>
</div>

View File

@@ -0,0 +1,61 @@
'use strict';
define([
'underscore',
'vent',
'AppLayout',
'marionette',
'Settings/Indexers/Restriction/RestrictionDeleteView',
'Commands/CommandController',
'Mixins/AsModelBoundView',
'Mixins/AsValidatedView',
'Mixins/AsEditModalView',
'Mixins/TagInput',
'bootstrap',
'bootstrap.tagsinput'
], function (_, vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, AsEditModalView) {
var view = Marionette.ItemView.extend({
template : 'Settings/Indexers/Restriction/RestrictionEditViewTemplate',
ui : {
required : '.x-required',
ignored : '.x-ignored',
tags : '.x-tags'
},
_deleteView: DeleteView,
initialize : function (options) {
this.targetCollection = options.targetCollection;
},
onRender : function () {
this.ui.required.tagsinput({
trimValue : true,
tagClass : 'label label-success'
});
this.ui.ignored.tagsinput({
trimValue : true,
tagClass : 'label label-danger'
});
this.ui.tags.tagInput({
model : this.model,
property : 'tags'
});
},
_onAfterSave : function () {
this.targetCollection.add(this.model, { merge : true });
vent.trigger(vent.Commands.CloseModalCommand);
}
});
AsModelBoundView.call(view);
AsValidatedView.call(view);
AsEditModalView.call(view);
return view;
});

View File

@@ -0,0 +1,60 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
{{#if id}}
<h3>Edit Restriction</h3>
{{else}}
<h3>Add Restriction</h3>
{{/if}}
</div>
<div class="modal-body remotepath-mapping-modal">
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">Must contain</label>
<div class="col-sm-1 col-sm-push-5 help-inline">
<i class="icon-nd-form-info" title="The release must contain at least one of these terms (case insensitive)" />
</div>
<div class="col-sm-5 col-sm-pull-1">
<input type="text" name="required" class="form-control x-required"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Must not contain</label>
<div class="col-sm-1 col-sm-push-5 help-inline">
<i class="icon-nd-form-info" title="The release will be rejected if it contains one or more of terms (case insensitive)" />
</div>
<div class="col-sm-5 col-sm-pull-1">
<input type="text" name="ignored" class="form-control x-ignored"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Tags</label>
<div class="col-sm-1 col-sm-push-5 help-inline">
<i class="icon-nd-form-info" title="Restrictions will apply to series with more or more matching tags. Leave blank to apply to all series" />
</div>
<div class="col-sm-5 col-sm-pull-1">
<input type="text" class="form-control x-tags">
</div>
</div>
</div>
</div>
<div class="modal-footer">
{{#if id}}
<button class="btn btn-danger pull-left x-delete">delete</button>
{{/if}}
<button class="btn" data-dismiss="modal">cancel</button>
<div class="btn-group">
<button class="btn btn-primary x-save">save</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
'use strict';
define([
'AppLayout',
'marionette',
'Settings/Indexers/Restriction/RestrictionEditView'
], function (AppLayout, Marionette, EditView) {
return Marionette.ItemView.extend({
template : 'Settings/Indexers/Restriction/RestrictionItemViewTemplate',
className : 'row',
ui: {
tags: '.x-tags'
},
events: {
'click .x-edit' : '_edit'
},
initialize: function () {
this.listenTo(this.model, 'sync', this.render);
},
_edit: function() {
var view = new EditView({ model: this.model, targetCollection: this.model.collection});
AppLayout.modalRegion.show(view);
}
});
});

View File

@@ -0,0 +1,12 @@
<span class="col-sm-4">
{{genericTagDisplay required 'label label-success'}}
</span>
<span class="col-sm-4">
{{genericTagDisplay ignored 'label label-danger'}}
</span>
<span class="col-sm-3">
{{tagDisplay tags}}
</span>
<span class="col-sm-1">
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit"></i></div>
</span>

View File

@@ -0,0 +1,10 @@
'use strict';
define([
'jquery',
'backbone.deepmodel'
], function ($, DeepModel) {
return DeepModel.DeepModel.extend({
});
});

View File

@@ -127,3 +127,34 @@ li.save-and-add:hover {
display : none;
padding-right : 5px;
}
.add-rule-setting-mapping {
cursor : pointer;
font-size : 14px;
text-align : center;
display : inline-block;
padding : 2px 6px;
i {
cursor : pointer;
}
}
.rule-setting-list {
.rule-setting-header .row {
font-weight : bold;
line-height : 40px;
}
.rows .row {
line-height : 30px;
border-top : 1px solid #ddd;
vertical-align : middle;
padding : 5px;
i {
cursor : pointer;
}
}
}

View File

@@ -1,20 +1,32 @@
'use strict';
define(
[
'underscore',
'handlebars',
'Tags/TagCollection'
], function (Handlebars, TagCollection) {
], function (_, Handlebars, TagCollection) {
Handlebars.registerHelper('tagInput', function () {
Handlebars.registerHelper('tagDisplay', function (tags) {
var unit = 'days';
var age = this.age;
var tagLabels = _.map(TagCollection.filter(function (tag) {
return _.contains(tags, tag.get('id'));
}), function (tag){
return '<span class="label label-info">{0}</span>'.format(tag.get('label'));
});
if (age < 2) {
unit = 'hours';
age = parseFloat(this.ageHours).toFixed(1);
return new Handlebars.SafeString(tagLabels.join(' '));
});
Handlebars.registerHelper('genericTagDisplay', function (tags, classes) {
if (!tags) {
return new Handlebars.SafeString('');
}
return new Handlebars.SafeString('<dt>Age (when grabbed):</dt><dd>{0} {1}</dd>'.format(age, unit));
var tagLabels = _.map(tags.split(','), function (tag) {
return '<span class="{0}">{1}</span>'.format(classes, tag);
});
return new Handlebars.SafeString(tagLabels.join(' '));
});
});

View File

@@ -1 +0,0 @@
<input type=""/>