From af4c35142801ec55abd32689601ab8a40915478d Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:19 +0100 Subject: [PATCH 1/4] Series Index can now be filtered and no longer fetches twice when starting. --- src/UI/Mixins/AsFilteredCollection.js | 78 +++++++++++++++++++ src/UI/Mixins/AsPersistedStateCollection.js | 31 ++++---- src/UI/Series/Index/SeriesIndexLayout.js | 69 +++++++++++++--- src/UI/Series/SeriesCollection.js | 17 +++- .../Shared/Toolbar/Radio/RadioButtonView.js | 6 +- src/UI/Shared/Toolbar/ToolbarLayout.js | 21 +++-- .../Shared/Toolbar/ToolbarLayoutTemplate.html | 10 +-- 7 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 src/UI/Mixins/AsFilteredCollection.js diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js new file mode 100644 index 000000000..447f60edb --- /dev/null +++ b/src/UI/Mixins/AsFilteredCollection.js @@ -0,0 +1,78 @@ +'use strict'; + +define( + [ + 'underscore', + 'backbone'], + function (_, Backbone) { + + return function () { + + this.prototype.setFilter = function(filter, options) { + options = _.extend({ reset: true }, options || {}); + + this.state.filterKey = filter[0]; + this.state.filterValue = filter[1]; + + if (options.reset) { + if (this.mode != 'server') { + this.fullCollection.resetFiltered(); + } else { + return this.fetch(); + } + } + }; + + this.prototype.setFilterMode = function(mode, options) { + this.setFilter(this.filterModes[mode], options); + }; + + var originalMakeFullCollection = this.prototype._makeFullCollection; + + this.prototype._makeFullCollection = function (models, options) { + var self = this; + + self.shadowCollection = originalMakeFullCollection.apply(this, [models, options]); + + var filterModel = function(model) { + if (!self.state.filterKey || !self.state.filterValue) + return true; + else + return model.get(self.state.filterKey) === self.state.filterValue; + }; + + self.shadowCollection.filtered = function() { + return this.filter(filterModel); + }; + + var filteredModels = self.shadowCollection.filtered(); + + var fullCollection = originalMakeFullCollection.apply(this, [filteredModels, options]); + + + fullCollection.resetFiltered = function(options) { + Backbone.Collection.prototype.reset.apply(this, [self.shadowCollection.filtered(), options]); + }; + + fullCollection.reset = function (models, options) { + self.shadowCollection.reset(models, options); + self.fullCollection.resetFiltered(); + }; + + return fullCollection; + }; + + _.extend(this.prototype.state, { + filterKey : null, + filterValue : null + }); + + _.extend(this.prototype.queryParams, { + filterKey : 'filterKey', + filterValue : 'filterValue' + }); + + return this; + }; + } +); \ No newline at end of file diff --git a/src/UI/Mixins/AsPersistedStateCollection.js b/src/UI/Mixins/AsPersistedStateCollection.js index 879c05427..d4177f1a4 100644 --- a/src/UI/Mixins/AsPersistedStateCollection.js +++ b/src/UI/Mixins/AsPersistedStateCollection.js @@ -62,23 +62,20 @@ define( return '1'; }; - - _.extend(this.prototype, { - initialSort: function () { - var key = this.state.sortKey; - var order = this.state.order; - - if (this[key] && this.mode === 'client') { - var sortValue = this[key]; - - this.setSorting(key, order, { sortValue: sortValue }); - - var comparator = this._makeComparator(key, order, sortValue); - this.fullCollection.comparator = comparator; - this.fullCollection.sort(); - } - } - }); + + var originalMakeComparator = this.prototype._makeComparator; + this.prototype._makeComparator = function (sortKey, order, sortValue) { + var state = this.state; + + sortKey = sortKey || state.sortKey; + order = order || state.order; + + if (!sortKey || !order) return; + + if (!sortValue && this[sortKey]) sortValue = this[sortKey]; + + return originalMakeComparator.call(this, sortKey, order, sortValue); + }; return this; }; diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 416d1ec1a..ff7eb55c1 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -167,6 +167,44 @@ define( this.listenTo(SeriesCollection, 'sync', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView); + this.filteringOptions = { + type : 'radio', + storeState : true, + menuKey : 'series.filterMode', + defaultAction: 'all', + items : + [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-circle-blank', + callback: this._setFilter + }, + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-nd-monitored', + callback: this._setFilter + }, + { + key : 'continuing', + title : '', + tooltip : 'Continuing Only', + icon : 'icon-play', + callback: this._setFilter + }, + { + key : 'ended', + title : '', + tooltip : 'Ended Only', + icon : 'icon-stop', + callback: this._setFilter + } + ] + }; + this.viewButtons = { type : 'radio', storeState : true, @@ -201,26 +239,30 @@ define( _showTable: function () { this.currentView = new Backgrid.Grid({ - collection: SeriesCollection, + collection: this.seriesCollection, columns : this.columns, className : 'table table-hover' }); - this._fetchCollection(); + this._renderView(); }, _showList: function () { - this.currentView = new ListCollectionView({ collection: SeriesCollection }); + this.currentView = new ListCollectionView({ + collection: this.seriesCollection + }); - this._fetchCollection(); + this._renderView(); }, _showPosters: function () { - this.currentView = new PosterCollectionView({ collection: SeriesCollection }); + this.currentView = new PosterCollectionView({ + collection: this.seriesCollection + }); - this._fetchCollection(); + this._renderView(); }, - + _renderView: function () { if (SeriesCollection.length === 0) { @@ -238,10 +280,17 @@ define( onShow: function () { this._showToolbar(); this._renderView(); + this._fetchCollection(); }, _fetchCollection: function () { - SeriesCollection.fetch(); + this.seriesCollection.fetch(); + }, + + _setFilter: function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.seriesCollection.setFilterMode(mode); }, _showToolbar: function () { @@ -251,11 +300,11 @@ define( } var rightButtons = [ + this.sortingOptions, + this.filteringOptions, this.viewButtons ]; - rightButtons.splice(0, 0, this.sortingOptions); - this.toolbar.show(new ToolbarLayout({ right : rightButtons, left : diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js index 629ed799e..4f00e6c42 100644 --- a/src/UI/Series/SeriesCollection.js +++ b/src/UI/Series/SeriesCollection.js @@ -6,9 +6,10 @@ define( 'backbone.pageable', 'Series/SeriesModel', 'api!series', + 'Mixins/AsFilteredCollection', 'Mixins/AsPersistedStateCollection', 'moment' - ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsPersistedStateCollection, Moment) { + ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsFilteredCollection, AsPersistedStateCollection, Moment) { var Collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/series', model: SeriesModel, @@ -47,6 +48,14 @@ define( return proxy.save(); }, + // Filter Modes + filterModes: { + 'all' : [null, null], + 'continuing' : ['status', 'continuing'], + 'ended' : ['status', 'ended'], + 'monitored' : ['monitored', true] + }, + //Sorters nextAiring: function (model, attr) { var nextAiring = model.get(attr); @@ -59,9 +68,9 @@ define( } }); - var MixedIn = AsPersistedStateCollection.call(Collection); - var collection = new MixedIn(SeriesData); - collection.initialSort(); + var FilteredCollection = AsFilteredCollection.call(Collection); + var MixedIn = AsPersistedStateCollection.call(FilteredCollection); + var collection = new MixedIn(SeriesData, { full: true }); return collection; }); diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index 1fb788250..baa67a5da 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -9,6 +9,10 @@ define( template : 'Shared/Toolbar/ButtonTemplate', className: 'btn', + ui: { + icon: 'i' + }, + events: { 'click': 'onClick' }, @@ -49,7 +53,7 @@ define( var callback = this.model.get('callback'); if (callback) { - callback.call(this.model.ownerContext); + callback.call(this.model.ownerContext, this); } } }); diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js index 8a907dad6..a50828880 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ b/src/UI/Shared/Toolbar/ToolbarLayout.js @@ -12,11 +12,9 @@ define( return Marionette.Layout.extend({ template: 'Shared/Toolbar/ToolbarLayoutTemplate', - regions: { - left_1 : '.x-toolbar-left-1', - left_2 : '.x-toolbar-left-2', - right_1: '.x-toolbar-right-1', - right_2: '.x-toolbar-right-2' + ui: { + left_x : '.x-toolbar-left', + right_x: '.x-toolbar-right' }, initialize: function (options) { @@ -97,8 +95,17 @@ define( break; } } - - this[position + '_' + (index + 1).toString()].show(buttonGroupView); + + var regionId = position + "_" + (index + 1); + var region = this[regionId]; + + if (!region) { + var regionClassName = "x-toolbar-" + position + "-" + (index + 1); + this.ui[position + '_x'].append('
\r\n'); + region = this.addRegion(regionId, "." + regionClassName); + } + + region.show(buttonGroupView); } }); }); diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html index b4cd4dcda..533f83bf9 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html +++ b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html @@ -1,8 +1,2 @@ -
-
-
-
-
-
-
-
+
+
From 9df0ad0bf7c2c96bcfb076d23361153f2184b01b Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:22 +0100 Subject: [PATCH 2/4] System.Logs view can now be filtered by severity. --- src/NzbDrone.Api/Logs/LogModule.cs | 25 +++++++++ src/NzbDrone.Api/PagingResource.cs | 2 + src/NzbDrone.Api/REST/RestModule.cs | 10 ++++ .../Datastore/BasicRepository.cs | 18 ++++--- .../History/HistoryRepository.cs | 18 ++----- src/UI/System/Logs/LogsCollection.js | 15 +++++- src/UI/System/Logs/Table/LogsTableLayout.js | 53 ++++++++++++++++++- 7 files changed, 116 insertions(+), 25 deletions(-) diff --git a/src/NzbDrone.Api/Logs/LogModule.cs b/src/NzbDrone.Api/Logs/LogModule.cs index 59ea4975d..8684e3250 100644 --- a/src/NzbDrone.Api/Logs/LogModule.cs +++ b/src/NzbDrone.Api/Logs/LogModule.cs @@ -23,6 +23,31 @@ namespace NzbDrone.Api.Logs pageSpec.SortKey = "id"; } + if (pagingResource.FilterKey == "level") + { + switch (pagingResource.FilterValue) + { + case "Fatal": + pageSpec.FilterExpression = h => h.Level == "Fatal"; + break; + case "Error": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error"; + break; + case "Warn": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn"; + break; + case "Info": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info"; + break; + case "Debug": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug"; + break; + case "Trace": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace"; + break; + } + } + return ApplyToPage(_logService.Paged, pageSpec); } } diff --git a/src/NzbDrone.Api/PagingResource.cs b/src/NzbDrone.Api/PagingResource.cs index ab53c24cd..96eeb7c48 100644 --- a/src/NzbDrone.Api/PagingResource.cs +++ b/src/NzbDrone.Api/PagingResource.cs @@ -9,6 +9,8 @@ namespace NzbDrone.Api public int PageSize { get; set; } public string SortKey { get; set; } public SortDirection SortDirection { get; set; } + public string FilterKey { get; set; } + public string FilterValue { get; set; } public int TotalRecords { get; set; } public List Records { get; set; } } diff --git a/src/NzbDrone.Api/REST/RestModule.cs b/src/NzbDrone.Api/REST/RestModule.cs index 1efcf0b3a..49cd14fa9 100644 --- a/src/NzbDrone.Api/REST/RestModule.cs +++ b/src/NzbDrone.Api/REST/RestModule.cs @@ -241,6 +241,16 @@ namespace NzbDrone.Api.REST } } + if (Request.Query.FilterKey != null) + { + pagingResource.FilterKey = Request.Query.FilterKey.ToString(); + + if (Request.Query.FilterValue != null) + { + pagingResource.FilterValue = Request.Query.FilterValue.ToString(); + } + } + return pagingResource; } } diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index f82a68354..f4125c0f2 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -210,18 +210,20 @@ namespace NzbDrone.Core.Datastore public virtual PagingSpec GetPaged(PagingSpec pagingSpec) { - var pagingQuery = Query.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); - - pagingSpec.Records = pagingQuery.ToList(); - - //TODO: Use the same query for count and records - pagingSpec.TotalRecords = Count(); + pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList(); + pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount(); return pagingSpec; } + protected virtual SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) + { + return query.Where(pagingSpec.FilterExpression) + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize); + } + public void DeleteAll() { DataMapper.Delete(c => c.Id > 0); diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 59d54c745..b1fdcad7a 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -67,22 +67,12 @@ namespace NzbDrone.Core.History .FirstOrDefault(); } - public override PagingSpec GetPaged(PagingSpec pagingSpec) + protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { - pagingSpec.Records = GetPagedQuery(pagingSpec).ToList(); - pagingSpec.TotalRecords = GetPagedQuery(pagingSpec).GetRowCount(); + var baseQuery = query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) + .Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id); - return pagingSpec; - } - - private SortBuilder GetPagedQuery(PagingSpec pagingSpec) - { - return Query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) - .Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id) - .Where(pagingSpec.FilterExpression) - .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); + return base.GetPagedQuery(baseQuery, pagingSpec); } } } \ No newline at end of file diff --git a/src/UI/System/Logs/LogsCollection.js b/src/UI/System/Logs/LogsCollection.js index 350ed1522..045675796 100644 --- a/src/UI/System/Logs/LogsCollection.js +++ b/src/UI/System/Logs/LogsCollection.js @@ -4,9 +4,10 @@ define( [ 'backbone.pageable', 'System/Logs/LogsModel', + 'Mixins/AsFilteredCollection', 'Mixins/AsPersistedStateCollection' ], - function (PagableCollection, LogsModel, AsPersistedStateCollection) { + function (PagableCollection, LogsModel, AsFilteredCollection, AsPersistedStateCollection) { var collection = PagableCollection.extend({ url : window.NzbDrone.ApiRoot + '/log', model: LogsModel, @@ -30,6 +31,14 @@ define( } }, + // Filter Modes + filterModes: { + 'all' : [null, null], + 'info' : ['level', 'Info'], + 'warn' : ['level', 'Warn'], + 'error' : ['level', 'Error'] + }, + parseState: function (resp, queryParams, state) { return {totalRecords: resp.totalRecords}; }, @@ -43,5 +52,7 @@ define( } }); - return AsPersistedStateCollection.call(collection); + collection = AsFilteredCollection.apply(collection); + + return AsPersistedStateCollection.apply(collection); }); diff --git a/src/UI/System/Logs/Table/LogsTableLayout.js b/src/UI/System/Logs/Table/LogsTableLayout.js index 219ebfc1b..9de384d68 100644 --- a/src/UI/System/Logs/Table/LogsTableLayout.js +++ b/src/UI/System/Logs/Table/LogsTableLayout.js @@ -66,7 +66,6 @@ define( onRender: function () { this.grid.show(new LoadingView()); - this.collection.fetch(); }, onShow: function () { @@ -88,6 +87,44 @@ define( }, _showToolbar: function () { + var filterButtons = { + type : 'radio', + storeState : true, + menuKey : 'logs.filterMode', + defaultAction: 'all', + items : + [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-circle-blank', + callback : this._setFilter + }, + { + key : 'info', + title : '', + tooltip : 'Info', + icon : 'icon-info', + callback : this._setFilter + }, + { + key : 'warn', + title : '', + tooltip : 'Warn', + icon : 'icon-warn', + callback : this._setFilter + }, + { + key : 'error', + title : '', + tooltip : 'Error', + icon : 'icon-error', + callback : this._setFilter + } + ] + }; + var rightSideButtons = { type : 'default', storeState: false, @@ -111,6 +148,7 @@ define( this.toolbar.show(new ToolbarLayout({ right : [ + filterButtons, rightSideButtons ], context: this @@ -125,6 +163,19 @@ define( buttonContext.ui.icon.spinForPromise(promise); } }, + + _setFilter: function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.collection.setFilterMode(mode, { reset: false }); + + this.collection.state.currentPage = 1; + var promise = this.collection.fetch({ reset: true }); + + if (buttonContext) { + buttonContext.ui.icon.spinForPromise(promise); + } + }, _commandComplete: function (options) { if (options.command.get('name') === 'clearlog') { From 7a4e05f04a0fb9c776375e5233a81b8d5547f881 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:25 +0100 Subject: [PATCH 3/4] Workaround to ensure the view uses a unique cloned collection for filtering instead of affecting the generic SeriesCollection. --- src/UI/Series/Index/SeriesIndexLayout.js | 70 ++++++++++++------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index ff7eb55c1..c5cc9ca51 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -127,46 +127,46 @@ define( ] }, - sortingOptions: { - type : 'sorting', - storeState : false, - viewCollection: SeriesCollection, - items : - [ - { - title: 'Title', - name : 'title' - }, - { - title: 'Seasons', - name : 'seasonCount' - }, - { - title: 'Quality', - name : 'qualityProfileId' - }, - { - title: 'Network', - name : 'network' - }, - { - title : 'Next Airing', - name : 'nextAiring', - sortValue : SeriesCollection.nextAiring - }, - { - title: 'Episodes', - name : 'percentOfEpisodes' - } - ] - }, - initialize: function () { - this.seriesCollection = SeriesCollection; + this.seriesCollection = SeriesCollection.clone(); this.listenTo(SeriesCollection, 'sync', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView); + this.sortingOptions = { + type : 'sorting', + storeState : false, + viewCollection: this.seriesCollection, + items : + [ + { + title: 'Title', + name : 'title' + }, + { + title: 'Seasons', + name : 'seasonCount' + }, + { + title: 'Quality', + name : 'qualityProfileId' + }, + { + title: 'Network', + name : 'network' + }, + { + title : 'Next Airing', + name : 'nextAiring', + sortValue : SeriesCollection.nextAiring + }, + { + title: 'Episodes', + name : 'percentOfEpisodes' + } + ] + }; + this.filteringOptions = { type : 'radio', storeState : true, From 98903869c32f0b8e02dfb97ef5866127c9eae0bb Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:30 +0100 Subject: [PATCH 4/4] Quick patch to solve Model.url issue. Should update to backbone 1.1.0 instead. --- src/UI/JsLibraries/backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/JsLibraries/backbone.js b/src/UI/JsLibraries/backbone.js index 3512d42fb..70a854d31 100644 --- a/src/UI/JsLibraries/backbone.js +++ b/src/UI/JsLibraries/backbone.js @@ -259,7 +259,7 @@ }; // A list of options to be attached directly to the model, if provided. - var modelOptions = ['url', 'urlRoot', 'collection']; + var modelOptions = ['urlRoot', 'collection']; // Attach all inheritable methods to the Model prototype. _.extend(Model.prototype, Events, {