=0&&s>h;a?h++:h--){var c=~~(h/o),d=h-c*o,u=this.rows[c].cells[d];if(u.column.get("renderable")&&u.column.get("editable")){u.enterEditMode();break}}}this.rows[n].cells[r].exitEditMode()}});h.Footer=n.View.extend({tagName:"tfoot",initialize:function(e){h.requireOptions(e,["columns","collection"]),this.columns=e.columns,this.columns instanceof n.Collection||(this.columns=new h.Columns(this.columns))}}),h.Grid=n.View.extend({tagName:"table",className:"backgrid",header:M,body:O,footer:null,initialize:function(e){h.requireOptions(e,["columns","collection"]),e.columns instanceof n.Collection||(e.columns=new $(e.columns)),this.columns=e.columns;var t=i.omit(e,["el","id","attributes","className","tagName","events"]);this.header=e.header||this.header,this.header=new this.header(t),this.body=e.body||this.body,this.body=new this.body(t),this.footer=e.footer||this.footer,this.footer&&(this.footer=new this.footer(t)),this.listenTo(this.columns,"reset",function(){this.header=new(this.header.remove().constructor)(t),this.body=new(this.body.remove().constructor)(t),this.footer&&(this.footer=new(this.footer.remove().constructor)(t)),this.render()})},insertRow:function(e,t,i){return this.body.insertRow(e,t,i)},removeRow:function(e,t,i){return this.body.removeRow(e,t,i)},insertColumn:function(e,t){return t=t||{render:!0},this.columns.add(e,t),this},removeColumn:function(e,t){return this.columns.remove(e,t),this},render:function(){return this.$el.empty(),this.$el.append(this.header.render().$el),this.footer&&this.$el.append(this.footer.render().$el),this.$el.append(this.body.render().$el),this.delegateEvents(),this.trigger("backgrid:rendered",this),this},remove:function(){return this.header.remove.apply(this.header,arguments),this.body.remove.apply(this.body,arguments),this.footer&&this.footer.remove.apply(this.footer,arguments),n.View.prototype.remove.apply(this,arguments)}})})(this,jQuery,_,Backbone);
\ No newline at end of file
diff --git a/static/js/libs/backgrid/extensions/filter/backgrid-filter.css b/static/js/libs/backgrid/extensions/filter/backgrid-filter.css
new file mode 100644
index 0000000..4f145ea
--- /dev/null
+++ b/static/js/libs/backgrid/extensions/filter/backgrid-filter.css
@@ -0,0 +1,19 @@
+/*
+ backgrid-filter
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT @license.
+*/
+
+.backgrid-filter .close {
+ display: inline-block;
+ float: none;
+ width: 20px;
+ height: 20px;
+ margin-top: -4px;
+ font-size: 20px;
+ line-height: 20px;
+ text-align: center;
+ vertical-align: text-top;
+}
diff --git a/static/js/libs/backgrid/extensions/filter/backgrid-filter.js b/static/js/libs/backgrid/extensions/filter/backgrid-filter.js
new file mode 100644
index 0000000..abc95a3
--- /dev/null
+++ b/static/js/libs/backgrid/extensions/filter/backgrid-filter.js
@@ -0,0 +1,365 @@
+/*
+ backgrid-filter
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT @license.
+*/
+
+(function ($, _, Backbone, Backgrid, lunr) {
+
+ "use strict";
+
+ /**
+ ServerSideFilter is a search form widget that submits a query to the server
+ for filtering the current collection.
+
+ @class Backgrid.Extension.ServerSideFilter
+ */
+ var ServerSideFilter = Backgrid.Extension.ServerSideFilter = Backbone.View.extend({
+
+ /** @property */
+ tagName: "form",
+
+ /** @property */
+ className: "backgrid-filter form-search",
+
+ /** @property {function(Object, ?Object=): string} template */
+ template: _.template(''),
+
+ /** @property */
+ events: {
+ "click .close": "clear",
+ "submit": "search"
+ },
+
+ /** @property {string} [name='q'] Query key */
+ name: "q",
+
+ /** @property The HTML5 placeholder to appear beneath the search box. */
+ placeholder: null,
+
+ /**
+ @param {Object} options
+ @param {Backbone.Collection} options.collection
+ @param {String} [options.name]
+ @param {String} [options.placeholder]
+ */
+ initialize: function (options) {
+ Backgrid.requireOptions(options, ["collection"]);
+ Backbone.View.prototype.initialize.apply(this, arguments);
+ this.name = options.name || this.name;
+ this.placeholder = options.placeholder || this.placeholder;
+
+ var collection = this.collection, self = this;
+ if (Backbone.PageableCollection &&
+ collection instanceof Backbone.PageableCollection &&
+ collection.mode == "server") {
+ collection.queryParams[this.name] = function () {
+ return self.$el.find("input[type=text]").val();
+ };
+ }
+ },
+
+ /**
+ Upon search form submission, this event handler constructs a query
+ parameter object and pass it to Collection#fetch for server-side
+ filtering.
+ */
+ search: function (e) {
+ if (e) e.preventDefault();
+ var data = {};
+ data[this.name] = this.$el.find("input[type=text]").val();
+ this.collection.fetch({data: data});
+ },
+
+ /**
+ Event handler for the close button. Clears the search box and refetch the
+ collection.
+ */
+ clear: function (e) {
+ if (e) e.preventDefault();
+ this.$("input[type=text]").val(null);
+ this.collection.fetch();
+ },
+
+ /**
+ Renders a search form with a text box, optionally with a placeholder and
+ a preset value if supplied during initialization.
+ */
+ render: function () {
+ this.$el.empty().append(this.template({
+ name: this.name,
+ placeholder: this.placeholder,
+ value: this.value
+ }));
+ this.delegateEvents();
+ return this;
+ }
+
+ });
+
+ /**
+ ClientSideFilter is a search form widget that searches a collection for
+ model matches against a query on the client side. The exact matching
+ algorithm can be overriden by subclasses.
+
+ @class Backgrid.Extension.ClientSideFilter
+ @extends Backgrid.Extension.ServerSideFilter
+ */
+ var ClientSideFilter = Backgrid.Extension.ClientSideFilter = ServerSideFilter.extend({
+
+ /** @property */
+ events: {
+ "click .close": function (e) {
+ e.preventDefault();
+ this.clear();
+ },
+ "change input[type=text]": "search",
+ "keyup input[type=text]": "search",
+ "submit": function (e) {
+ e.preventDefault();
+ this.search();
+ }
+ },
+
+ /**
+ @property {?Array.} A list of model field names to search
+ for matches. If null, all of the fields will be searched.
+ */
+ fields: null,
+
+ /**
+ @property wait The time in milliseconds to wait since for since the last
+ change to the search box's value before searching. This value can be
+ adjusted depending on how often the search box is used and how large the
+ search index is.
+ */
+ wait: 149,
+
+ /**
+ Debounces the #search and #clear methods and makes a copy of the given
+ collection for searching.
+
+ @param {Object} options
+ @param {Backbone.Collection} options.collection
+ @param {String} [options.placeholder]
+ @param {String} [options.fields]
+ @param {String} [options.wait=149]
+ */
+ initialize: function (options) {
+ ServerSideFilter.prototype.initialize.apply(this, arguments);
+
+ this.fields = options.fields || this.fields;
+ this.wait = options.wait || this.wait;
+
+ this._debounceMethods(["search", "clear"]);
+
+ var collection = this.collection;
+ var shadowCollection = this.shadowCollection = collection.clone();
+ shadowCollection.url = collection.url;
+ shadowCollection.sync = collection.sync;
+ shadowCollection.parse = collection.parse;
+
+ this.listenTo(collection, "add", function (model, collection, options) {
+ shadowCollection.add(model, options);
+ });
+ this.listenTo(collection, "remove", function (model, collection, options) {
+ shadowCollection.remove(model, options);
+ });
+ this.listenTo(collection, "sort reset", function (collection, options) {
+ options = _.extend({reindex: true}, options || {});
+ if (options.reindex) shadowCollection.reset(collection.models);
+ });
+ },
+
+ _debounceMethods: function (methodNames) {
+ if (_.isString(methodNames)) methodNames = [methodNames];
+
+ this.undelegateEvents();
+
+ for (var i = 0, l = methodNames.length; i < l; i++) {
+ var methodName = methodNames[i];
+ var method = this[methodName];
+ this[methodName] = _.debounce(method, this.wait);
+ }
+
+ this.delegateEvents();
+ },
+
+ /**
+ This default implementation takes a query string and returns a matcher
+ function that looks for matches in the model's #fields or all of its
+ fields if #fields is null, for any of the words in the query
+ case-insensitively.
+
+ Subclasses overriding this method must take care to conform to the
+ signature of the matcher function. In addition, when the matcher function
+ is called, its context will be bound to this ClientSideFilter object so
+ it has access to the filter's attributes and methods.
+
+ @param {string} query The search query in the search box.
+ @return {function(Backbone.Model):boolean} A matching function.
+ */
+ makeMatcher: function (query) {
+ var regexp = new RegExp(query.trim().split(/\W/).join("|"), "i");
+ return function (model) {
+ var keys = this.fields || model.keys();
+ for (var i = 0, l = keys.length; i < l; i++) {
+ if (regexp.test(model.get(keys[i]) + "")) return true;
+ }
+ return false;
+ };
+ },
+
+ /**
+ Takes the query from the search box, constructs a matcher with it and
+ loops through collection looking for matches. Reset the given collection
+ when all the matches have been found.
+ */
+ search: function () {
+ var matcher = _.bind(this.makeMatcher(this.$("input[type=text]").val()), this);
+ this.collection.reset(this.shadowCollection.filter(matcher), {reindex: false});
+ },
+
+ /**
+ Clears the search box and reset the collection to its original.
+ */
+ clear: function () {
+ this.$("input[type=text]").val(null);
+ this.collection.reset(this.shadowCollection.models, {reindex: false});
+ }
+
+ });
+
+ /**
+ LunrFilter is a ClientSideFilter that uses [lunrjs](http://lunrjs.com/) to
+ index the text fields of each model for a collection, and performs
+ full-text searching.
+
+ @class Backgrid.Extension.LunrFilter
+ @extends Backgrid.Extension.ClientSideFilter
+ */
+ Backgrid.Extension.LunrFilter = ClientSideFilter.extend({
+
+ /**
+ @property {string} [ref="id"]`lunrjs` document reference attribute name.
+ */
+ ref: "id",
+
+ /**
+ @property {Object} fields A hash of `lunrjs` index field names and boost
+ value. Unlike ClientSideFilter#fields, LunrFilter#fields is _required_ to
+ initialize the index.
+ */
+ fields: null,
+
+ /**
+ Indexes the underlying collection on construction. The index will refresh
+ when the underlying collection is reset. If any model is added, removed
+ or if any indexed fields of any models has changed, the index will be
+ updated.
+
+ @param {Object} options
+ @param {Backbone.Collection} options.collection
+ @param {String} [options.placeholder]
+ @param {string} [options.ref] `lunrjs` document reference attribute name.
+ @param {Object} [options.fields] A hash of `lunrjs` index field names and
+ boost value.
+ @param {number} [options.wait]
+ */
+ initialize: function (options) {
+ ClientSideFilter.prototype.initialize.apply(this, arguments);
+
+ this.ref = options.ref || this.ref;
+
+ var collection = this.collection;
+ this.listenTo(collection, "add", this.addToIndex);
+ this.listenTo(collection, "remove", this.removeFromIndex);
+ this.listenTo(collection, "reset", this.resetIndex);
+ this.listenTo(collection, "change", this.updateIndex);
+
+ this.resetIndex(collection);
+ },
+
+ /**
+ Reindex the collection. If `options.reindex` is `false`, this method is a
+ no-op.
+
+ @param {Backbone.Collection} collection
+ @param {Object} [options]
+ @param {boolean} [options.reindex=true]
+ */
+ resetIndex: function (collection, options) {
+ options = _.extend({reindex: true}, options || {});
+
+ if (options.reindex) {
+ var self = this;
+ this.index = lunr(function () {
+ _.each(self.fields, function (boost, fieldName) {
+ this.field(fieldName, boost);
+ this.ref(self.ref);
+ }, this);
+ });
+
+ collection.each(function (model) {
+ this.addToIndex(model);
+ }, this);
+ }
+ },
+
+ /**
+ Adds the given model to the index.
+
+ @param {Backbone.Model} model
+ */
+ addToIndex: function (model) {
+ var index = this.index;
+ var doc = model.toJSON();
+ if (index.documentStore.has(doc[this.ref])) index.update(doc);
+ else index.add(doc);
+ },
+
+ /**
+ Removes the given model from the index.
+
+ @param {Backbone.Model} model
+ */
+ removeFromIndex: function (model) {
+ var index = this.index;
+ var doc = model.toJSON();
+ if (index.documentStore.has(doc[this.ref])) index.remove(doc);
+ },
+
+ /**
+ Updates the index for the given model.
+
+ @param {Backbone.Model} model
+ */
+ updateIndex: function (model) {
+ var changed = model.changedAttributes();
+ if (changed && !_.isEmpty(_.intersection(_.keys(this.fields),
+ _.keys(changed)))) {
+ this.index.update(model.toJSON());
+ }
+ },
+
+ /**
+ Takes the query from the search box and performs a full-text search on
+ the client-side. The search result is returned by resetting the
+ underlying collection to the models after interrogating the index for the
+ query answer.
+ */
+ search: function () {
+ var searchResults = this.index.search(this.$("input[type=text]").val());
+ var models = [];
+ for (var i = 0; i < searchResults.length; i++) {
+ var result = searchResults[i];
+ models.push(this.shadowCollection.get(result.ref));
+ }
+ this.collection.reset(models, {reindex: false});
+ }
+
+ });
+
+}(jQuery, _, Backbone, Backgrid, lunr));
diff --git a/static/js/libs/backgrid/extensions/filter/backgrid-filter.min.css b/static/js/libs/backgrid/extensions/filter/backgrid-filter.min.css
new file mode 100644
index 0000000..58c85a3
--- /dev/null
+++ b/static/js/libs/backgrid/extensions/filter/backgrid-filter.min.css
@@ -0,0 +1,8 @@
+/*
+ backgrid-filter
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT @license.
+*/
+.backgrid-filter .close{display:inline-block;float:none;width:20px;height:20px;margin-top:-4px;font-size:20px;line-height:20px;text-align:center;vertical-align:text-top}
diff --git a/static/js/libs/backgrid/extensions/filter/backgrid-filter.min.js b/static/js/libs/backgrid/extensions/filter/backgrid-filter.min.js
new file mode 100644
index 0000000..d8c2767
--- /dev/null
+++ b/static/js/libs/backgrid/extensions/filter/backgrid-filter.min.js
@@ -0,0 +1,8 @@
+/*
+ backgrid-filter
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT @license.
+*/
+(function(e,t,i,n,s){"use strict";var a=n.Extension.ServerSideFilter=i.View.extend({tagName:"form",className:"backgrid-filter form-search",template:t.template(''),events:{"click .close":"clear",submit:"search"},name:"q",placeholder:null,initialize:function(e){n.requireOptions(e,["collection"]),i.View.prototype.initialize.apply(this,arguments),this.name=e.name||this.name,this.placeholder=e.placeholder||this.placeholder;var t=this.collection,s=this;i.PageableCollection&&t instanceof i.PageableCollection&&t.mode=="server"&&(t.queryParams[this.name]=function(){return s.$el.find("input[type=text]").val()})},search:function(e){e&&e.preventDefault();var t={};t[this.name]=this.$el.find("input[type=text]").val(),this.collection.fetch({data:t})},clear:function(e){e&&e.preventDefault(),this.$("input[type=text]").val(null),this.collection.fetch()},render:function(){return this.$el.empty().append(this.template({name:this.name,placeholder:this.placeholder,value:this.value})),this.delegateEvents(),this}}),l=n.Extension.ClientSideFilter=a.extend({events:{"click .close":function(e){e.preventDefault(),this.clear()},"change input[type=text]":"search","keyup input[type=text]":"search",submit:function(e){e.preventDefault(),this.search()}},fields:null,wait:149,initialize:function(e){a.prototype.initialize.apply(this,arguments),this.fields=e.fields||this.fields,this.wait=e.wait||this.wait,this._debounceMethods(["search","clear"]);var i=this.collection,n=this.shadowCollection=i.clone();n.url=i.url,n.sync=i.sync,n.parse=i.parse,this.listenTo(i,"add",function(e,t,i){n.add(e,i)}),this.listenTo(i,"remove",function(e,t,i){n.remove(e,i)}),this.listenTo(i,"sort reset",function(e,i){i=t.extend({reindex:!0},i||{}),i.reindex&&n.reset(e.models)})},_debounceMethods:function(e){t.isString(e)&&(e=[e]),this.undelegateEvents();for(var i=0,n=e.length;n>i;i++){var s=e[i],a=this[s];this[s]=t.debounce(a,this.wait)}this.delegateEvents()},makeMatcher:function(e){var t=new RegExp(e.trim().split(/\W/).join("|"),"i");return function(e){for(var i=this.fields||e.keys(),n=0,s=i.length;s>n;n++)if(t.test(e.get(i[n])+""))return!0;return!1}},search:function(){var e=t.bind(this.makeMatcher(this.$("input[type=text]").val()),this);this.collection.reset(this.shadowCollection.filter(e),{reindex:!1})},clear:function(){this.$("input[type=text]").val(null),this.collection.reset(this.shadowCollection.models,{reindex:!1})}});n.Extension.LunrFilter=l.extend({ref:"id",fields:null,initialize:function(e){l.prototype.initialize.apply(this,arguments),this.ref=e.ref||this.ref;var t=this.collection;this.listenTo(t,"add",this.addToIndex),this.listenTo(t,"remove",this.removeFromIndex),this.listenTo(t,"reset",this.resetIndex),this.listenTo(t,"change",this.updateIndex),this.resetIndex(t)},resetIndex:function(e,i){if(i=t.extend({reindex:!0},i||{}),i.reindex){var n=this;this.index=s(function(){t.each(n.fields,function(e,t){this.field(t,e),this.ref(n.ref)},this)}),e.each(function(e){this.addToIndex(e)},this)}},addToIndex:function(e){var t=this.index,i=e.toJSON();t.documentStore.has(i[this.ref])?t.update(i):t.add(i)},removeFromIndex:function(e){var t=this.index,i=e.toJSON();t.documentStore.has(i[this.ref])&&t.remove(i)},updateIndex:function(e){var i=e.changedAttributes();i&&!t.isEmpty(t.intersection(t.keys(this.fields),t.keys(i)))&&this.index.update(e.toJSON())},search:function(){for(var e=this.index.search(this.$("input[type=text]").val()),t=[],i=0;i li {
+ display: inline;
+}
+
+.backgrid-paginator ul > li > a,
+.backgrid-paginator ul > li > span {
+ float: left;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ line-height: 30px;
+ text-decoration: none;
+}
+
+.backgrid-paginator ul > li > a:hover,
+.backgrid-paginator ul > .active > a,
+.backgrid-paginator ul > .active > span {
+ background-color: #f5f5f5;
+}
+
+.backgrid-paginator ul > .active > a,
+.backgrid-paginator ul > .active > span {
+ color: #999999;
+ cursor: default;
+}
+
+.backgrid-paginator ul > .disabled > span,
+.backgrid-paginator ul > .disabled > a,
+.backgrid-paginator ul > .disabled > a:hover {
+ color: #999999;
+ cursor: default;
+}
diff --git a/static/js/libs/backgrid/extensions/paginator/backgrid-paginator.js b/static/js/libs/backgrid/extensions/paginator/backgrid-paginator.js
new file mode 100644
index 0000000..cceabca
--- /dev/null
+++ b/static/js/libs/backgrid/extensions/paginator/backgrid-paginator.js
@@ -0,0 +1,198 @@
+/*
+ backgrid-paginator
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT @license.
+*/
+
+(function ($, _, Backbone, Backgrid) {
+
+ "use strict";
+
+ /**
+ Paginator is a Backgrid extension that renders a series of configurable
+ pagination handles. This extension is best used for splitting a large data
+ set across multiple pages. If the number of pages is larger then a
+ threshold, which is set to 10 by default, the page handles are rendered
+ within a sliding window, plus the fast forward, fast backward, previous and
+ next page handles. The fast forward, fast backward, previous and next page
+ handles can be turned off.
+
+ @class Backgrid.Extension.Paginator
+ */
+ Backgrid.Extension.Paginator = Backbone.View.extend({
+
+ /** @property */
+ className: "backgrid-paginator",
+
+ /** @property */
+ windowSize: 10,
+
+ /**
+ @property {Object} fastForwardHandleLabels You can disable specific
+ handles by setting its value to `null`.
+ */
+ fastForwardHandleLabels: {
+ first: "《",
+ prev: "〈",
+ next: "〉",
+ last: "》"
+ },
+
+ /** @property */
+ template: _.template(''),
+
+ /** @property */
+ events: {
+ "click a": "changePage"
+ },
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backbone.Collection} options.collection
+ @param {boolean} [options.fastForwardHandleLabels] Whether to render fast forward buttons.
+ */
+ initialize: function (options) {
+ Backgrid.requireOptions(options, ["collection"]);
+
+ var collection = this.collection;
+ var fullCollection = collection.fullCollection;
+ if (fullCollection) {
+ this.listenTo(fullCollection, "add", this.render);
+ this.listenTo(fullCollection, "remove", this.render);
+ this.listenTo(fullCollection, "reset", this.render);
+ }
+ else {
+ this.listenTo(collection, "add", this.render);
+ this.listenTo(collection, "remove", this.render);
+ this.listenTo(collection, "reset", this.render);
+ }
+ },
+
+ /**
+ jQuery event handler for the page handlers. Goes to the right page upon
+ clicking.
+
+ @param {Event} e
+ */
+ changePage: function (e) {
+ e.preventDefault();
+
+ var $li = $(e.target).parent();
+ if (!$li.hasClass("active") && !$li.hasClass("disabled")) {
+
+ var label = $(e.target).text();
+ var ffLabels = this.fastForwardHandleLabels;
+
+ var collection = this.collection;
+
+ if (ffLabels) {
+ switch (label) {
+ case ffLabels.first:
+ collection.getFirstPage();
+ return;
+ case ffLabels.prev:
+ collection.getPreviousPage();
+ return;
+ case ffLabels.next:
+ collection.getNextPage();
+ return;
+ case ffLabels.last:
+ collection.getLastPage();
+ return;
+ }
+ }
+
+ var state = collection.state;
+ var pageIndex = +label;
+ collection.getPage(state.firstPage === 0 ? pageIndex - 1 : pageIndex);
+ }
+ },
+
+ /**
+ Internal method to create a list of page handle objects for the template
+ to render them.
+
+ @return {Array.