Commit de5e15b
Changed files (7)
templates
audit/1387332439.147935.86003
@@ -0,0 +1,1 @@
+{"transferid": "1387332439.147935.86003", "username": "user", "warnings": ["xxxx", "xxxx", "xxxx"], "error": false, "filenames": ["3.txt", "1.txt", "2.txt"]}
\ No newline at end of file
audit/1387343428.412962.14809
@@ -0,0 +1,1 @@
+{"transferid": "1387343428.412962.14809", "username": "userb", "warnings": ["xxxx", "xxxx"], "error": false, "filenames": ["out.gif", "KH8fCJb.jpg"]}
\ No newline at end of file
static/jquery.dynatable.css
@@ -0,0 +1,48 @@
+th a {
+ color: #fff;
+}
+th a:hover {
+ color: #fff;
+ text-decoration: underline;
+}
+
+.dynatable-search {
+ float: right;
+ margin-bottom: 10px;
+}
+
+.dynatable-pagination-links {
+ float: right;
+}
+
+.dynatable-record-count {
+ display: block;
+ padding: 5px 0;
+}
+
+.dynatable-pagination-links span,
+.dynatable-pagination-links li {
+ display: inline-block;
+}
+
+.dynatable-page-link,
+.dynatable-page-break {
+ display: block;
+ padding: 5px 7px;
+}
+
+.dynatable-page-link {
+ cursor: pointer;
+}
+
+.dynatable-active-page {
+ background: #71AF5A;
+ border-radius: 5px;
+ color: #fff;
+}
+.dynatable-page-prev.dynatable-active-page,
+.dynatable-page-next.dynatable-active-page {
+ background: none;
+ color: #999;
+ cursor: text;
+}
static/jquery.dynatable.js
@@ -0,0 +1,1658 @@
+/*
+ * jQuery Dynatable plugin 0.2.2
+ *
+ * Copyright (c) 2013 Steve Schwartz (JangoSteve)
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Date: Tue Aug 13 12:50:00 2013 -0500
+ */
+//
+
+(function($) {
+ var defaults,
+ mergeSettings,
+ dt,
+ Model,
+ modelPrototypes = {
+ dom: Dom,
+ domColumns: DomColumns,
+ records: Records,
+ recordsCount: RecordsCount,
+ processingIndicator: ProcessingIndicator,
+ state: State,
+ sorts: Sorts,
+ sortsHeaders: SortsHeaders,
+ queries: Queries,
+ inputsSearch: InputsSearch,
+ paginationPage: PaginationPage,
+ paginationPerPage: PaginationPerPage,
+ paginationLinks: PaginationLinks
+ },
+ utility,
+ build,
+ processAll,
+ initModel,
+ defaultRowWriter,
+ defaultCellWriter,
+ defaultAttributeWriter,
+ defaultAttributeReader;
+
+ //-----------------------------------------------------------------
+ // Cached plugin global defaults
+ //-----------------------------------------------------------------
+
+ defaults = {
+ features: {
+ paginate: true,
+ sort: true,
+ pushState: true,
+ search: true,
+ recordCount: true,
+ perPageSelect: true
+ },
+ table: {
+ defaultColumnIdStyle: 'camelCase',
+ columns: null,
+ headRowSelector: 'thead tr', // or e.g. tr:first-child
+ bodyRowSelector: 'tbody tr',
+ headRowClass: null
+ },
+ inputs: {
+ queries: null,
+ sorts: null,
+ multisort: ['ctrlKey', 'shiftKey', 'metaKey'],
+ page: null,
+ queryEvent: 'blur change',
+ recordCountTarget: null,
+ recordCountPlacement: 'after',
+ paginationLinkTarget: null,
+ paginationLinkPlacement: 'after',
+ paginationPrev: 'Previous',
+ paginationNext: 'Next',
+ paginationGap: [1,2,2,1],
+ searchTarget: null,
+ searchPlacement: 'before',
+ perPageTarget: null,
+ perPagePlacement: 'before',
+ perPageText: 'Show: ',
+ recordCountText: 'Showing ',
+ processingText: 'Processing...'
+ },
+ dataset: {
+ ajax: false,
+ ajaxUrl: null,
+ ajaxCache: null,
+ ajaxOnLoad: false,
+ ajaxMethod: 'GET',
+ ajaxDataType: 'json',
+ totalRecordCount: null,
+ queries: {},
+ queryRecordCount: null,
+ page: null,
+ perPageDefault: 10,
+ perPageOptions: [10,20,50,100],
+ sorts: {},
+ sortsKeys: null,
+ sortTypes: {},
+ records: null
+ },
+ writers: {
+ _rowWriter: defaultRowWriter,
+ _cellWriter: defaultCellWriter,
+ _attributeWriter: defaultAttributeWriter
+ },
+ readers: {
+ _rowReader: null,
+ _attributeReader: defaultAttributeReader
+ },
+ params: {
+ dynatable: 'dynatable',
+ queries: 'queries',
+ sorts: 'sorts',
+ page: 'page',
+ perPage: 'perPage',
+ offset: 'offset',
+ records: 'records',
+ record: null,
+ queryRecordCount: 'queryRecordCount',
+ totalRecordCount: 'totalRecordCount'
+ }
+ };
+
+ //-----------------------------------------------------------------
+ // Each dynatable instance inherits from this,
+ // set properties specific to instance
+ //-----------------------------------------------------------------
+
+ dt = {
+ init: function(element, options) {
+ this.settings = mergeSettings(options);
+ this.element = element;
+ this.$element = $(element);
+
+ // All the setup that doesn't require element or options
+ build.call(this);
+
+ return this;
+ },
+
+ process: function(skipPushState) {
+ processAll.call(this, skipPushState);
+ }
+ };
+
+ //-----------------------------------------------------------------
+ // Cached plugin global functions
+ //-----------------------------------------------------------------
+
+ mergeSettings = function(options) {
+ var newOptions = $.extend(true, {}, defaults, options);
+
+ // TODO: figure out a better way to do this.
+ // Doing `extend(true)` causes any elements that are arrays
+ // to merge the default and options arrays instead of overriding the defaults.
+ if (options) {
+ if (options.inputs) {
+ if (options.inputs.multisort) {
+ newOptions.inputs.multisort = options.inputs.multisort;
+ }
+ if (options.inputs.paginationGap) {
+ newOptions.inputs.paginationGap = options.inputs.paginationGap;
+ }
+ }
+ if (options.dataset && options.dataset.perPageOptions) {
+ newOptions.dataset.perPageOptions = options.dataset.perPageOptions;
+ }
+ }
+
+ return newOptions;
+ };
+
+ build = function() {
+ for (model in modelPrototypes) {
+ if (modelPrototypes.hasOwnProperty(model)) {
+ var modelInstance = this[model] = new modelPrototypes[model](this, this.settings);
+ if (modelInstance.initOnLoad()) {
+ modelInstance.init();
+ }
+ }
+ }
+
+ this.$element.trigger('dynatable:init', this);
+
+ if (!this.settings.dataset.ajax || (this.settings.dataset.ajax && this.settings.dataset.ajaxOnLoad) || this.settings.features.paginate) {
+ this.process();
+ }
+ };
+
+ processAll = function(skipPushState) {
+ var data = {};
+
+ this.$element.trigger('dynatable:beforeProcess', data);
+
+ if (!$.isEmptyObject(this.settings.dataset.queries)) { data[this.settings.params.queries] = this.settings.dataset.queries; }
+ // TODO: Wrap this in a try/rescue block to hide the processing indicator and indicate something went wrong if error
+ this.processingIndicator.show();
+
+ if (this.settings.features.sort && !$.isEmptyObject(this.settings.dataset.sorts)) { data[this.settings.params.sorts] = this.settings.dataset.sorts; }
+ if (this.settings.features.paginate && this.settings.dataset.page) {
+ var page = this.settings.dataset.page,
+ perPage = this.settings.dataset.perPage;
+ data[this.settings.params.page] = page;
+ data[this.settings.params.perPage] = perPage;
+ data[this.settings.params.offset] = (page - 1) * perPage;
+ }
+ if (this.settings.dataset.ajaxData) { $.extend(data, this.settings.dataset.ajaxData); }
+
+ // If ajax, sends query to ajaxUrl with queries and sorts serialized and appended in ajax data
+ // otherwise, executes queries and sorts on in-page data
+ if (this.settings.dataset.ajax) {
+ var _this = this;
+ var options = {
+ type: _this.settings.dataset.ajaxMethod,
+ dataType: _this.settings.dataset.ajaxDataType,
+ data: data,
+ error: function(xhr, error) {
+ },
+ success: function(response) {
+ _this.$element.trigger('dynatable:ajax:success', response);
+ // Merge ajax results and meta-data into dynatables cached data
+ _this.records.updateFromJson(response);
+ // update table with new records
+ _this.dom.update();
+
+ if (!skipPushState && _this.state.initOnLoad()) {
+ _this.state.push(data);
+ }
+ },
+ complete: function() {
+ _this.processingIndicator.hide();
+ }
+ };
+ // Do not pass url to `ajax` options if blank
+ if (this.settings.dataset.ajaxUrl) {
+ options.url = this.settings.dataset.ajaxUrl;
+
+ // If ajaxUrl is blank, then we're using the current page URL,
+ // we need to strip out any query, sort, or page data controlled by dynatable
+ // that may have been in URL when page loaded, so that it doesn't conflict with
+ // what's passed in with the data ajax parameter
+ } else {
+ options.url = utility.refreshQueryString(window.location.href, {}, this.settings);
+ }
+ if (this.settings.dataset.ajaxCache !== null) { options.cache = this.settings.dataset.ajaxCache; }
+
+ $.ajax(options);
+ } else {
+ this.records.resetOriginal();
+ this.queries.run();
+ if (this.settings.features.sort) {
+ this.records.sort();
+ }
+ if (this.settings.features.paginate) {
+ this.records.paginate();
+ }
+ this.dom.update();
+ this.processingIndicator.hide();
+
+ if (!skipPushState && this.state.initOnLoad()) {
+ this.state.push(data);
+ }
+ }
+ this.$element.trigger('dynatable:afterProcess', data);
+ };
+
+ function defaultRowWriter(rowIndex, record, columns, cellWriter) {
+ var tr = '';
+
+ // grab the record's attribute for each column
+ for (var i = 0, len = columns.length; i < len; i++) {
+ tr += cellWriter(columns[i], record);
+ }
+
+ return '<tr>' + tr + '</tr>';
+ };
+
+ function defaultCellWriter(column, record) {
+ var html = column.attributeWriter(record),
+ td = '<td';
+
+ // keep cells for hidden column headers hidden
+ if (column.hidden) {
+ td += ' display="none"';
+ }
+
+ // keep cells aligned as their column headers are aligned
+ if (column.textAlign) {
+ td += ' style="text-align: ' + column.textAlign + ';"';
+ }
+
+ return td + '>' + html + '</td>';
+ };
+
+ function defaultAttributeWriter(record) {
+ // `this` is the column object in settings.columns
+ // TODO: automatically convert common types, such as arrays and objects, to string
+ return record[this.id];
+ };
+
+ function defaultAttributeReader(cell, record) {
+ return $(cell).html();
+ };
+
+ //-----------------------------------------------------------------
+ // Dynatable object model prototype
+ // (all object models get these default functions)
+ //-----------------------------------------------------------------
+
+ Model = {
+ initOnLoad: function() {
+ return true;
+ },
+
+ init: function() {}
+ };
+
+ for (model in modelPrototypes) {
+ if (modelPrototypes.hasOwnProperty(model)) {
+ var modelPrototype = modelPrototypes[model];
+ modelPrototype.prototype = Model;
+ }
+ }
+
+ //-----------------------------------------------------------------
+ // Dynatable object models
+ //-----------------------------------------------------------------
+
+ function Dom(obj, settings) {
+ var _this = this;
+
+ // update table contents with new records array
+ // from query (whether ajax or not)
+ this.update = function() {
+ var rows = '',
+ columns = settings.table.columns,
+ rowWriter = settings.writers._rowWriter,
+ cellWriter = settings.writers._cellWriter;
+
+ obj.$element.trigger('dynatable:beforeUpdate', rows);
+
+ // loop through records
+ for (var i = 0, len = settings.dataset.records.length; i < len; i++) {
+ var record = settings.dataset.records[i],
+ tr = rowWriter(i, record, columns, cellWriter);
+ rows += tr;
+ }
+
+ // Appended dynatable interactive elements
+ if (settings.features.recordCount) {
+ $('#dynatable-record-count-' + obj.element.id).replaceWith(obj.recordsCount.create());
+ }
+ if (settings.features.paginate) {
+ $('#dynatable-pagination-links-' + obj.element.id).replaceWith(obj.paginationLinks.create());
+ if (settings.features.perPageSelect) {
+ $('#dynatable-per-page-' + obj.element.id).val(parseInt(settings.dataset.perPage));
+ }
+ }
+
+ // Sort headers functionality
+ if (settings.features.sort && columns) {
+ obj.sortsHeaders.removeAllArrows();
+ for (var i = 0, len = columns.length; i < len; i++) {
+ var column = columns[i],
+ sortedByColumn = utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; }),
+ value = settings.dataset.sorts[column.sorts[0]];
+
+ if (sortedByColumn) {
+ obj.$element.find('[data-dynatable-column="' + column.id + '"]').find('.dynatable-sort-header').each(function(){
+ if (value == 1) {
+ obj.sortsHeaders.appendArrowUp($(this));
+ } else {
+ obj.sortsHeaders.appendArrowDown($(this));
+ }
+ });
+ }
+ }
+ }
+
+ // Query search functionality
+ if (settings.inputs.queries) {
+ settings.inputs.queries.each(function() {
+ var $this = $(this),
+ q = settings.dataset.queries[$this.data('dynatable-query')];
+ $(this).val(q || '');
+ });
+ }
+ obj.$element.find(settings.table.bodyRowSelector).remove();
+ obj.$element.append(rows);
+
+ obj.$element.trigger('dynatable:afterUpdate', rows);
+ };
+ };
+
+ function DomColumns(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return obj.$element.is('table');
+ };
+
+ this.init = function() {
+ settings.table.columns = [];
+ this.getFromTable();
+ };
+
+ // initialize table[columns] array
+ this.getFromTable = function() {
+ var $columns = obj.$element.find(settings.table.headRowSelector).children('th,td');
+ if ($columns.length) {
+ $columns.each(function(index){
+ _this.add($(this), index, true);
+ });
+ } else {
+ return $.error("Couldn't find any columns headers in '" + settings.table.headRowSelector + " th,td'. If your header row is different, specify the selector in the table: headRowSelector option.");
+ }
+ };
+
+ this.add = function($column, position, skipAppend, skipUpdate) {
+ var columns = settings.table.columns,
+ label = $column.text(),
+ id = $column.data('dynatable-column') || utility.normalizeText(label, settings.table.defaultColumnIdStyle),
+ dataSorts = $column.data('dynatable-sorts'),
+ sorts = dataSorts ? $.map(dataSorts.split(','), function(text) { return $.trim(text); }) : [id];
+
+ // If the column id is blank, generate an id for it
+ if ( !id ) {
+ this.generate($column);
+ id = $column.data('dynatable-column');
+ }
+ // Add column data to plugin instance
+ columns.splice(position, 0, {
+ index: position,
+ label: label,
+ id: id,
+ attributeWriter: settings.writers[id] || settings.writers._attributeWriter,
+ attributeReader: settings.readers[id] || settings.readers._attributeReader,
+ sorts: sorts,
+ hidden: $column.css('display') === 'none',
+ textAlign: $column.css('text-align')
+ });
+
+ // Modify header cell
+ $column
+ .attr('data-dynatable-column', id)
+ .addClass('dynatable-head');
+ if (settings.table.headRowClass) { $column.addClass(settings.table.headRowClass); }
+
+ // Append column header to table
+ if (!skipAppend) {
+ var domPosition = position + 1,
+ $sibling = obj.$element.find(settings.table.headRowSelector)
+ .children('th:nth-child(' + domPosition + '),td:nth-child(' + domPosition + ')').first(),
+ columnsAfter = columns.slice(position + 1, columns.length);
+
+ if ($sibling.length) {
+ $sibling.before($column);
+ // sibling column doesn't yet exist (maybe this is the last column in the header row)
+ } else {
+ obj.$element.find(settings.table.headRowSelector).append($column);
+ }
+
+ obj.sortsHeaders.attachOne($column.get());
+
+ // increment the index of all columns after this one that was just inserted
+ if (columnsAfter.length) {
+ for (var i = 0, len = columnsAfter.length; i < len; i++) {
+ columnsAfter[i].index += 1;
+ }
+ }
+
+ if (!skipUpdate) {
+ obj.dom.update();
+ }
+ }
+
+ return dt;
+ };
+
+ this.remove = function(columnIndexOrId) {
+ var columns = settings.table.columns,
+ length = columns.length;
+
+ if (typeof(columnIndexOrId) === "number") {
+ var column = columns[columnIndexOrId];
+ this.removeFromTable(column.id);
+ this.removeFromArray(columnIndexOrId);
+ } else {
+ // Traverse columns array in reverse order so that subsequent indices
+ // don't get messed up when we delete an item from the array in an iteration
+ for (var i = columns.length - 1; i >= 0; i--) {
+ var column = columns[i];
+
+ if (column.id === columnIndexOrId) {
+ this.removeFromTable(columnIndexOrId);
+ this.removeFromArray(i);
+ }
+ }
+ }
+
+ obj.dom.update();
+ };
+
+ this.removeFromTable = function(columnId) {
+ obj.$element.find(settings.table.headRowSelector).children('[data-dynatable-column="' + columnId + '"]').first()
+ .remove();
+ };
+
+ this.removeFromArray = function(index) {
+ var columns = settings.table.columns,
+ adjustColumns;
+ columns.splice(index, 1);
+ adjustColumns = columns.slice(index, columns.length);
+ for (var i = 0, len = adjustColumns.length; i < len; i++) {
+ adjustColumns[i].index -= 1;
+ }
+ };
+
+ this.generate = function($cell) {
+ var cell = $cell === undefined ? $('<th></th>') : $cell;
+ return this.attachGeneratedAttributes(cell);
+ };
+
+ this.attachGeneratedAttributes = function($cell) {
+ // Use increment to create unique column name that is the same each time the page is reloaded,
+ // in order to avoid errors with mismatched attribute names when loading cached `dataset.records` array
+ var increment = obj.$element.find(settings.table.headRowSelector).children('th[data-dynatable-generated]').length;
+ return $cell
+ .attr('data-dynatable-column', 'dynatable-generated-' + increment) //+ utility.randomHash(),
+ .attr('data-dynatable-no-sort', 'true')
+ .attr('data-dynatable-generated', increment);
+ };
+ };
+
+ function Records(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return !settings.dataset.ajax;
+ };
+
+ this.init = function() {
+ if (settings.dataset.records === null) {
+ settings.dataset.records = this.getFromTable();
+
+ if (!settings.dataset.queryRecordCount) {
+ settings.dataset.queryRecordCount = this.count();
+ }
+
+ if (!settings.dataset.totalRecordCount){
+ settings.dataset.totalRecordCount = settings.dataset.queryRecordCount;
+ }
+ }
+
+ // Create cache of original full recordset (unpaginated and unqueried)
+ settings.dataset.originalRecords = $.extend(true, [], settings.dataset.records);
+ };
+
+ // merge ajax response json with cached data including
+ // meta-data and records
+ this.updateFromJson = function(data) {
+ var records;
+ if (settings.params.records === "_root") {
+ records = data;
+ } else if (settings.params.records in data) {
+ records = data[settings.params.records];
+ }
+ if (settings.params.record) {
+ var len = records.length - 1;
+ for (var i = 0; i < len; i++) {
+ records[i] = records[i][settings.params.record];
+ }
+ }
+ if (settings.params.queryRecordCount in data) {
+ settings.dataset.queryRecordCount = data[settings.params.queryRecordCount];
+ }
+ if (settings.params.totalRecordCount in data) {
+ settings.dataset.totalRecordCount = data[settings.params.totalRecordCount];
+ }
+ settings.dataset.records = records;
+ };
+
+ // For really advanced sorting,
+ // see http://james.padolsey.com/javascript/sorting-elements-with-jquery/
+ this.sort = function() {
+ var sort = [].sort,
+ sorts = settings.dataset.sorts,
+ sortsKeys = settings.dataset.sortsKeys,
+ sortTypes = settings.dataset.sortTypes;
+
+ var sortFunction = function(a, b) {
+ var comparison;
+ if ($.isEmptyObject(sorts)) {
+ comparison = obj.sorts.functions['originalPlacement'](a, b);
+ } else {
+ for (var i = 0, len = sortsKeys.length; i < len; i++) {
+ var attr = sortsKeys[i],
+ direction = sorts[attr],
+ sortType = sortTypes[attr] || obj.sorts.guessType(a, b, attr);
+ comparison = obj.sorts.functions[sortType](a, b, attr, direction);
+ // Don't need to sort any further unless this sort is a tie between a and b,
+ // so break the for loop unless tied
+ if (comparison !== 0) { break; }
+ }
+ }
+ return comparison;
+ }
+
+ return sort.call(settings.dataset.records, sortFunction);
+ };
+
+ this.paginate = function() {
+ var bounds = this.pageBounds(),
+ first = bounds[0], last = bounds[1];
+ settings.dataset.records = settings.dataset.records.slice(first, last);
+ };
+
+ this.resetOriginal = function() {
+ settings.dataset.records = settings.dataset.originalRecords || [];
+ };
+
+ this.pageBounds = function() {
+ var page = settings.dataset.page || 1,
+ first = (page - 1) * settings.dataset.perPage,
+ last = Math.min(first + settings.dataset.perPage, settings.dataset.queryRecordCount);
+ return [first,last];
+ };
+
+ // get initial recordset to populate table
+ // if ajax, call ajaxUrl
+ // otherwise, initialize from in-table records
+ this.getFromTable = function() {
+ var records = [],
+ columns = settings.table.columns,
+ tableRecords = obj.$element.find(settings.table.bodyRowSelector);
+
+ tableRecords.each(function(index){
+ var record = {};
+ record['dynatable-original-index'] = index;
+ $(this).find('th,td').each(function(index) {
+ if (columns[index] === undefined) {
+ // Header cell didn't exist for this column, so let's generate and append
+ // a new header cell with a randomly generated name (so we can store and
+ // retrieve the contents of this column for each record)
+ obj.domColumns.add(obj.domColumns.generate(), columns.length, false, true); // don't skipAppend, do skipUpdate
+ }
+ var value = columns[index].attributeReader(this, record),
+ attr = columns[index].id;
+
+ // If value from table is HTML, let's get and cache the text equivalent for
+ // the default string sorting, since it rarely makes sense for sort headers
+ // to sort based on HTML tags.
+ if (typeof(value) === "string" && value.match(/\s*\<.+\>/)) {
+ if (! record['dynatable-sortable-text']) {
+ record['dynatable-sortable-text'] = {};
+ }
+ record['dynatable-sortable-text'][attr] = $.trim($('<div></div>').html(value).text());
+ }
+
+ record[attr] = value;
+ });
+ // Allow configuration function which alters record based on attributes of
+ // table row (e.g. from html5 data- attributes)
+ if (typeof(settings.readers._rowReader) === "function") {
+ settings.readers._rowReader(index, this, record);
+ }
+ records.push(record);
+ });
+ return records; // 1st row is header
+ };
+
+ // count records from table
+ this.count = function() {
+ return settings.dataset.records.length;
+ };
+ };
+
+ function RecordsCount(obj, settings) {
+ this.initOnLoad = function() {
+ return settings.features.recordCount;
+ };
+
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function() {
+ var recordsShown = obj.records.count(),
+ recordsQueryCount = settings.dataset.queryRecordCount,
+ recordsTotal = settings.dataset.totalRecordCount,
+ text = settings.inputs.recordCountText,
+ collection_name = settings.params.records;
+
+ if (recordsShown < recordsQueryCount && settings.features.paginate) {
+ var bounds = obj.records.pageBounds();
+ text += "<span class='dynatable-record-bounds'>" + (bounds[0] + 1) + " to " + bounds[1] + "</span> of ";
+ } else if (recordsShown === recordsQueryCount && settings.features.paginate) {
+ text += recordsShown + " of ";
+ }
+ text += recordsQueryCount + " " + collection_name;
+ if (recordsQueryCount < recordsTotal) {
+ text += " (filtered from " + recordsTotal + " total records)";
+ }
+
+ return $('<span></span>', {
+ id: 'dynatable-record-count-' + obj.element.id,
+ 'class': 'dynatable-record-count',
+ html: text
+ });
+ };
+
+ this.attach = function() {
+ var $target = settings.inputs.recordCountTarget ? $(settings.inputs.recordCountTarget) : obj.$element;
+ $target[settings.inputs.recordCountPlacement](this.create());
+ };
+ };
+
+ function ProcessingIndicator(obj, settings) {
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function() {
+ var $processing = $('<div></div>', {
+ html: '<span>' + settings.inputs.processingText + '</span>',
+ id: 'dynatable-processing-' + obj.element.id,
+ 'class': 'dynatable-processing',
+ style: 'position: absolute; display: none;'
+ });
+
+ return $processing;
+ };
+
+ this.position = function() {
+ var $processing = $('#dynatable-processing-' + obj.element.id),
+ $span = $processing.children('span'),
+ spanHeight = $span.outerHeight(),
+ spanWidth = $span.outerWidth(),
+ $covered = obj.$element,
+ offset = $covered.offset(),
+ height = $covered.outerHeight(), width = $covered.outerWidth();
+
+ $processing
+ .offset({left: offset.left, top: offset.top})
+ .width(width)
+ .height(height)
+ $span
+ .offset({left: offset.left + ( (width - spanWidth) / 2 ), top: offset.top + ( (height - spanHeight) / 2 )});
+
+ return $processing;
+ };
+
+ this.attach = function() {
+ obj.$element.before(this.create());
+ };
+
+ this.show = function() {
+ $('#dynatable-processing-' + obj.element.id).show();
+ this.position();
+ };
+
+ this.hide = function() {
+ $('#dynatable-processing-' + obj.element.id).hide();
+ };
+ };
+
+ function State(obj, settings) {
+ this.initOnLoad = function() {
+ // Check if pushState option is true, and if browser supports it
+ return settings.features.pushState && history.pushState;
+ };
+
+ this.init = function() {
+ window.onpopstate = function(event) {
+ if (event.state && event.state.dynatable) {
+ obj.state.pop(event);
+ }
+ }
+ };
+
+ this.push = function(data) {
+ var urlString = window.location.search,
+ urlOptions,
+ path,
+ params,
+ hash,
+ newParams,
+ cacheStr,
+ cache,
+ // replaceState on initial load, then pushState after that
+ firstPush = !(window.history.state && window.history.state.dynatable),
+ pushFunction = firstPush ? 'replaceState' : 'pushState';
+
+ if (urlString && /^\?/.test(urlString)) { urlString = urlString.substring(1); }
+ $.extend(urlOptions, data);
+
+ params = utility.refreshQueryString(urlString, data, settings);
+ if (params) { params = '?' + params; }
+ hash = window.location.hash;
+ path = window.location.pathname;
+
+ obj.$element.trigger('dynatable:push', data);
+
+ cache = { dynatable: { dataset: settings.dataset } };
+ if (!firstPush) { cache.dynatable.scrollTop = $(window).scrollTop(); }
+ cacheStr = JSON.stringify(cache);
+
+ // Mozilla has a 640k char limit on what can be stored in pushState.
+ // See "limit" in https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
+ // and "dataStr.length" in http://wine.git.sourceforge.net/git/gitweb.cgi?p=wine/wine-gecko;a=patch;h=43a11bdddc5fc1ff102278a120be66a7b90afe28
+ //
+ // Likewise, other browsers may have varying (undocumented) limits.
+ // Also, Firefox's limit can be changed in about:config as browser.history.maxStateObjectSize
+ // Since we don't know what the actual limit will be in any given situation, we'll just try caching and rescue
+ // any exceptions by retrying pushState without caching the records.
+ //
+ // I have aboslutely no idea why perPageOptions suddenly becomes an array-like object instead of an array,
+ // but just recently, this started throwing an error if I don't convert it:
+ // 'Uncaught Error: DATA_CLONE_ERR: DOM Exception 25'
+ cache.dynatable.dataset.perPageOptions = $.makeArray(cache.dynatable.dataset.perPageOptions);
+
+ try {
+ window.history[pushFunction](cache, "Dynatable state", path + params + hash);
+ } catch(error) {
+ // Make cached records = null, so that `pop` will rerun process to retrieve records
+ cache.dynatable.dataset.records = null;
+ window.history[pushFunction](cache, "Dynatable state", path + params + hash);
+ }
+ };
+
+ this.pop = function(event) {
+ var data = event.state.dynatable;
+ settings.dataset = data.dataset;
+
+ if (data.scrollTop) { $(window).scrollTop(data.scrollTop); }
+
+ // If dataset.records is cached from pushState
+ if ( data.dataset.records ) {
+ obj.dom.update();
+ } else {
+ obj.process(true);
+ }
+ };
+ };
+
+ function Sorts(obj, settings) {
+ this.initOnLoad = function() {
+ return settings.features.sort;
+ };
+
+ this.init = function() {
+ var sortsUrl = window.location.search.match(new RegExp(settings.params.sorts + '[^&=]*=[^&]*', 'g'));
+ settings.dataset.sorts = sortsUrl ? utility.deserialize(sortsUrl)[settings.params.sorts] : {};
+ settings.dataset.sortsKeys = sortsUrl ? utility.keysFromObject(settings.dataset.sorts) : [];
+ };
+
+ this.add = function(attr, direction) {
+ var sortsKeys = settings.dataset.sortsKeys,
+ index = $.inArray(attr, sortsKeys);
+ settings.dataset.sorts[attr] = direction;
+ if (index === -1) { sortsKeys.push(attr); }
+ return dt;
+ };
+
+ this.remove = function(attr) {
+ var sortsKeys = settings.dataset.sortsKeys,
+ index = $.inArray(attr, sortsKeys);
+ delete settings.dataset.sorts[attr];
+ if (index !== -1) { sortsKeys.splice(index, 1); }
+ return dt;
+ };
+
+ this.clear = function() {
+ settings.dataset.sorts = {};
+ settings.dataset.sortsKeys.length = 0;
+ };
+
+ // Try to intelligently guess which sort function to use
+ // based on the type of attribute values.
+ // Consider using something more robust than `typeof` (http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/)
+ this.guessType = function(a, b, attr) {
+ var types = {
+ string: 'string',
+ number: 'number',
+ 'boolean': 'number',
+ object: 'number' // dates and null values are also objects, this works...
+ },
+ attrType = a[attr] ? typeof(a[attr]) : typeof(b[attr]),
+ type = types[attrType] || 'number';
+ return type;
+ };
+
+ // Built-in sort functions
+ // (the most common use-cases I could think of)
+ this.functions = {
+ number: function(a, b, attr, direction) {
+ return a[attr] === b[attr] ? 0 : (direction > 0 ? a[attr] - b[attr] : b[attr] - a[attr]);
+ },
+ string: function(a, b, attr, direction) {
+ var aAttr = (a['dynatable-sortable-text'] && a['dynatable-sortable-text'][attr]) ? a['dynatable-sortable-text'][attr] : a[attr],
+ bAttr = (b['dynatable-sortable-text'] && b['dynatable-sortable-text'][attr]) ? b['dynatable-sortable-text'][attr] : b[attr],
+ comparison;
+ aAttr = aAttr.toLowerCase();
+ bAttr = bAttr.toLowerCase();
+ comparison = aAttr === bAttr ? 0 : (direction > 0 ? aAttr > bAttr : bAttr > aAttr);
+ // force false boolean value to -1, true to 1, and tie to 0
+ return comparison === false ? -1 : (comparison - 0);
+ },
+ originalPlacement: function(a, b) {
+ return a['dynatable-original-index'] - b['dynatable-original-index'];
+ }
+ };
+ };
+
+ // turn table headers into links which add sort to sorts array
+ function SortsHeaders(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.features.sort;
+ };
+
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function(cell) {
+ var $cell = $(cell),
+ $link = $('<a></a>', {
+ 'class': 'dynatable-sort-header',
+ href: '#',
+ html: $cell.html()
+ }),
+ id = $cell.data('dynatable-column'),
+ column = utility.findObjectInArray(settings.table.columns, {id: id});
+
+ $link.bind('click', function(e) {
+ _this.toggleSort(e, $link, column);
+ obj.process();
+
+ e.preventDefault();
+ });
+
+ if (this.sortedByColumn($link, column)) {
+ if (this.sortedByColumnValue(column) == 1) {
+ this.appendArrowUp($link);
+ } else {
+ this.appendArrowDown($link);
+ }
+ }
+
+ return $link;
+ };
+
+ this.removeAll = function() {
+ obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
+ _this.removeAllArrows();
+ _this.removeOne(this);
+ });
+ };
+
+ this.removeOne = function(cell) {
+ var $cell = $(cell),
+ $link = $cell.find('.dynatable-sort-header');
+ if ($link.length) {
+ var html = $link.html();
+ $link.remove();
+ $cell.html($cell.html() + html);
+ }
+ };
+
+ this.attach = function() {
+ obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
+ _this.attachOne(this);
+ });
+ };
+
+ this.attachOne = function(cell) {
+ var $cell = $(cell);
+ if (!$cell.data('dynatable-no-sort')) {
+ $cell.html(this.create(cell));
+ }
+ };
+
+ this.appendArrowUp = function($link) {
+ this.removeArrow($link);
+ $link.append("<span class='dynatable-arrow'> ▲</span>");
+ };
+
+ this.appendArrowDown = function($link) {
+ this.removeArrow($link);
+ $link.append("<span class='dynatable-arrow'> ▼</span>");
+ };
+
+ this.removeArrow = function($link) {
+ // Not sure why `parent()` is needed, the arrow should be inside the link from `append()` above
+ $link.find('.dynatable-arrow').remove();
+ };
+
+ this.removeAllArrows = function() {
+ obj.$element.find('.dynatable-arrow').remove();
+ };
+
+ this.toggleSort = function(e, $link, column) {
+ var sortedByColumn = this.sortedByColumn($link, column),
+ value = this.sortedByColumnValue(column);
+ // Clear existing sorts unless this is a multisort event
+ if (!settings.inputs.multisort || !utility.anyMatch(e, settings.inputs.multisort, function(evt, key) { return e[key]; })) {
+ this.removeAllArrows();
+ obj.sorts.clear();
+ }
+
+ // If sorts for this column are already set
+ if (sortedByColumn) {
+ // If ascending, then make descending
+ if (value == 1) {
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
+ obj.sorts.add(column.sorts[i], -1);
+ }
+ this.appendArrowDown($link);
+ // If descending, remove sort
+ } else {
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
+ obj.sorts.remove(column.sorts[i]);
+ }
+ this.removeArrow($link);
+ }
+ // Otherwise, if not already set, set to ascending
+ } else {
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
+ obj.sorts.add(column.sorts[i], 1);
+ }
+ this.appendArrowUp($link);
+ }
+ };
+
+ this.sortedByColumn = function($link, column) {
+ return utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; });
+ };
+
+ this.sortedByColumnValue = function(column) {
+ return settings.dataset.sorts[column.sorts[0]];
+ };
+ };
+
+ function Queries(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.inputs.queries || settings.features.search;
+ };
+
+ this.init = function() {
+ var queriesUrl = window.location.search.match(new RegExp(settings.params.queries + '[^&=]*=[^&]*', 'g'));
+
+ settings.dataset.queries = queriesUrl ? utility.deserialize(queriesUrl)[settings.params.queries] : {};
+ if (settings.dataset.queries === "") { settings.dataset.queries = {}; }
+
+ if (settings.inputs.queries) {
+ this.setupInputs();
+ }
+ };
+
+ this.add = function(name, value) {
+ // reset to first page since query will change records
+ if (settings.features.paginate) {
+ settings.dataset.page = 1;
+ }
+ settings.dataset.queries[name] = value;
+ return dt;
+ };
+
+ this.remove = function(name) {
+ delete settings.dataset.queries[name];
+ return dt;
+ };
+
+ this.run = function() {
+ for (query in settings.dataset.queries) {
+ if (settings.dataset.queries.hasOwnProperty(query)) {
+ var value = settings.dataset.queries[query];
+ if (_this.functions[query] === undefined) {
+ // Try to lazily evaluate query from column names if not explictly defined
+ var queryColumn = utility.findObjectInArray(settings.table.columns, {id: query});
+ if (queryColumn) {
+ _this.functions[query] = function(record, queryValue) {
+ return record[query] == queryValue;
+ };
+ } else {
+ $.error("Query named '" + query + "' called, but not defined in queries.functions");
+ continue; // to skip to next query
+ }
+ }
+ // collect all records that return true for query
+ settings.dataset.records = $.map(settings.dataset.records, function(record) {
+ return _this.functions[query](record, value) ? record : null;
+ });
+ }
+ }
+ settings.dataset.queryRecordCount = obj.records.count();
+ };
+
+ // Shortcut for performing simple query from built-in search
+ this.runSearch = function(q) {
+ var origQueries = $.extend({}, settings.dataset.queries);
+ if (q) {
+ this.add('search', q);
+ } else {
+ this.remove('search');
+ }
+ if (!utility.objectsEqual(settings.dataset.queries, origQueries)) {
+ obj.process();
+ }
+ };
+
+ this.setupInputs = function() {
+ settings.inputs.queries.each(function() {
+ var $this = $(this),
+ event = $this.data('dynatable-query-event') || settings.inputs.queryEvent,
+ query = $this.data('dynatable-query') || $this.attr('name') || this.id,
+ queryFunction = function(e) {
+ var q = $(this).val();
+ if (q === "") { q = undefined; }
+ if (q === settings.dataset.queries[query]) { return false; }
+ if (q) {
+ _this.add(query, q);
+ } else {
+ _this.remove(query);
+ }
+ obj.process();
+ e.preventDefault();
+ };
+
+ $this
+ .attr('data-dynatable-query', query)
+ .bind(event, queryFunction)
+ .bind('keypress', function(e) {
+ if (e.which == 13) {
+ queryFunction.call(this, e);
+ }
+ });
+
+ if (settings.dataset.queries[query]) { $this.val(decodeURIComponent(settings.dataset.queries[query])); }
+ });
+ };
+
+ // Query functions for in-page querying
+ // each function should take a record and a value as input
+ // and output true of false as to whether the record is a match or not
+ this.functions = {
+ search: function(record, queryValue) {
+ var contains = false;
+ // Loop through each attribute of record
+ for (attr in record) {
+ if (record.hasOwnProperty(attr)) {
+ var attrValue = record[attr];
+ if (typeof(attrValue) === "string" && attrValue.toLowerCase().indexOf(queryValue.toLowerCase()) !== -1) {
+ contains = true;
+ // Don't need to keep searching attributes once found
+ break;
+ } else {
+ continue;
+ }
+ }
+ }
+ return contains;
+ }
+ };
+ };
+
+ function InputsSearch(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.features.search;
+ };
+
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function() {
+ var $search = $('<input />', {
+ type: 'search',
+ id: 'dynatable-query-search-' + obj.element.id,
+ value: settings.dataset.queries.search
+ }),
+ $searchSpan = $('<span></span>', {
+ id: 'dynatable-search-' + obj.element.id,
+ 'class': 'dynatable-search',
+ text: 'Search: '
+ }).append($search);
+
+ $search
+ .bind(settings.inputs.queryEvent, function() {
+ obj.queries.runSearch($(this).val());
+ })
+ .bind('keypress', function(e) {
+ if (e.which == 13) {
+ obj.queries.runSearch($(this).val());
+ e.preventDefault();
+ }
+ });
+ return $searchSpan;
+ };
+
+ this.attach = function() {
+ var $target = settings.inputs.searchTarget ? $(settings.inputs.searchTarget) : obj.$element;
+ $target[settings.inputs.searchPlacement](this.create());
+ };
+ };
+
+ // provide a public function for selecting page
+ function PaginationPage(obj, settings) {
+ this.initOnLoad = function() {
+ return settings.features.paginate;
+ };
+
+ this.init = function() {
+ var pageUrl = window.location.search.match(new RegExp(settings.params.page + '=([^&]*)'));
+ // If page is present in URL parameters and pushState is enabled
+ // (meaning that it'd be possible for dynatable to have put the
+ // page parameter in the URL)
+ if (pageUrl && settings.features.pushState) {
+ this.set(pageUrl[1]);
+ } else {
+ this.set(1);
+ }
+ };
+
+ this.set = function(page) {
+ settings.dataset.page = parseInt(page, 10);
+ }
+ };
+
+ function PaginationPerPage(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.features.paginate;
+ };
+
+ this.init = function() {
+ var perPageUrl = window.location.search.match(new RegExp(settings.params.perPage + '=([^&]*)'));
+
+ // If perPage is present in URL parameters and pushState is enabled
+ // (meaning that it'd be possible for dynatable to have put the
+ // perPage parameter in the URL)
+ if (perPageUrl && settings.features.pushState) {
+ // Don't reset page to 1 on init, since it might override page
+ // set on init from URL
+ this.set(perPageUrl[1], true);
+ } else {
+ this.set(settings.dataset.perPageDefault, true);
+ }
+
+ if (settings.features.perPageSelect) {
+ this.attach();
+ }
+ };
+
+ this.create = function() {
+ var $select = $('<select>', {
+ id: 'dynatable-per-page-' + obj.element.id,
+ 'class': 'dynatable-per-page-select'
+ });
+
+ for (var i = 0, len = settings.dataset.perPageOptions.length; i < len; i++) {
+ var number = settings.dataset.perPageOptions[i],
+ selected = settings.dataset.perPage == number ? 'selected="selected"' : '';
+ $select.append('<option value="' + number + '" ' + selected + '>' + number + '</option>');
+ }
+
+ $select.bind('change', function(e) {
+ _this.set($(this).val());
+ obj.process();
+ });
+
+ return $('<span />', {
+ 'class': 'dynatable-per-page'
+ }).append("<span class='dynatable-per-page-label'>" + settings.inputs.perPageText + "</span>").append($select);
+ };
+
+ this.attach = function() {
+ var $target = settings.inputs.perPageTarget ? $(settings.inputs.perPageTarget) : obj.$element;
+ $target[settings.inputs.perPagePlacement](this.create());
+ };
+
+ this.set = function(number, skipResetPage) {
+ if (!skipResetPage) { obj.paginationPage.set(1); }
+ settings.dataset.perPage = parseInt(number);
+ };
+ };
+
+ // pagination links which update dataset.page attribute
+ function PaginationLinks(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.features.paginate;
+ };
+
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function() {
+ var $pageLinks = $('<ul></ul>', {
+ id: 'dynatable-pagination-links-' + obj.element.id,
+ 'class': 'dynatable-pagination-links',
+ html: '<span>Pages: </span>'
+ }),
+ pageLinkClass = 'dynatable-page-link',
+ activePageClass = 'dynatable-active-page',
+ pages = Math.ceil(settings.dataset.queryRecordCount / settings.dataset.perPage),
+ page = settings.dataset.page,
+ breaks = [
+ settings.inputs.paginationGap[0],
+ settings.dataset.page - settings.inputs.paginationGap[1],
+ settings.dataset.page + settings.inputs.paginationGap[2],
+ (pages + 1) - settings.inputs.paginationGap[3]
+ ],
+ $link;
+
+ for (var i = 1; i <= pages; i++) {
+ if ( (i > breaks[0] && i < breaks[1]) || (i > breaks[2] && i < breaks[3])) {
+ // skip to next iteration in loop
+ continue;
+ } else {
+ $link = $('<a></a>',{
+ html: i,
+ 'class': pageLinkClass,
+ 'data-dynatable-page': i
+ }).appendTo($pageLinks);
+
+ if (page == i) { $link.addClass(activePageClass); }
+
+ // If i is not between one of the following
+ // (1 + (settings.paginationGap[0]))
+ // (page - settings.paginationGap[1])
+ // (page + settings.paginationGap[2])
+ // (pages - settings.paginationGap[3])
+ var breakIndex = $.inArray(i, breaks),
+ nextBreak = breaks[breakIndex + 1];
+ if (breakIndex > 0 && i !== 1 && nextBreak && nextBreak > (i + 1)) {
+ var $ellip = $('<span class="dynatable-page-break">…</span>');
+ $link = breakIndex < 2 ? $link.before($ellip) : $link.after($ellip);
+ }
+
+ }
+
+ if (settings.inputs.paginationPrev && i === 1) {
+ var $prevLink = $('<a></a>',{
+ html: settings.inputs.paginationPrev,
+ 'class': pageLinkClass + ' dynatable-page-prev',
+ 'data-dynatable-page': page - 1
+ });
+ if (page === 1) { $prevLink.addClass(activePageClass); }
+ $link = $link.before($prevLink);
+ }
+ if (settings.inputs.paginationNext && i === pages) {
+ var $nextLink = $('<a></a>',{
+ html: settings.inputs.paginationNext,
+ 'class': pageLinkClass + ' dynatable-page-next',
+ 'data-dynatable-page': page + 1
+ });
+ if (page === pages) { $nextLink.addClass(activePageClass); }
+ $link = $link.after($nextLink);
+ }
+ }
+
+ $pageLinks.children().wrap('<li></li>');
+
+ // only bind page handler to non-active pages
+ var selector = '#dynatable-pagination-links-' + obj.element.id + ' .' + pageLinkClass + ':not(.' + activePageClass + ')';
+ // kill any existing delegated-bindings so they don't stack up
+ $(document).undelegate(selector, 'click.dynatable');
+ $(document).delegate(selector, 'click.dynatable', function(e) {
+ $this = $(this);
+ $this.closest('.dynatable-pagination-links').find('.' + activePageClass).removeClass(activePageClass);
+ $this.addClass(activePageClass);
+
+ obj.paginationPage.set($this.data('dynatable-page'));
+ obj.process();
+ e.preventDefault();
+ });
+
+ return $pageLinks;
+ };
+
+ this.attach = function() {
+ // append page liks *after* delegate-event-binding so it doesn't need to
+ // find and select all page links to bind event
+ var $target = settings.inputs.paginationLinkTarget ? $(settings.inputs.paginationLinkTarget) : obj.$element;
+ $target[settings.inputs.paginationLinkPlacement](obj.paginationLinks.create());
+ };
+ };
+
+ utility = {
+ normalizeText: function(text, style) {
+ text = this.textTransform[style](text);
+ return text;
+ },
+ textTransform: {
+ trimDash: function(text) {
+ return text.replace(/^\s+|\s+$/g, "").replace(/\s+/g, "-");
+ },
+ camelCase: function(text) {
+ text = this.trimDash(text);
+ return text
+ .replace(/(\-[a-zA-Z])/g, function($1){return $1.toUpperCase().replace('-','');})
+ .replace(/([A-Z])([A-Z]+)/g, function($1,$2,$3){return $2 + $3.toLowerCase();})
+ .replace(/^[A-Z]/, function($1){return $1.toLowerCase();});
+ },
+ dashed: function(text) {
+ text = this.trimDash(text);
+ return this.lowercase(text);
+ },
+ underscore: function(text) {
+ text = this.trimDash(text);
+ return this.lowercase(text.replace(/(-)/g, '_'));
+ },
+ lowercase: function(text) {
+ return text.replace(/([A-Z])/g, function($1){return $1.toLowerCase();});
+ }
+ },
+ // Deserialize params in URL to object
+ // see http://stackoverflow.com/questions/1131630/javascript-jquery-param-inverse-function/3401265#3401265
+ deserialize: function(query) {
+ if (!query) return {};
+ // modified to accept an array of partial URL strings
+ if (typeof(query) === "object") { query = query.join('&'); }
+
+ var hash = {},
+ vars = query.split("&");
+
+ for (var i = 0; i < vars.length; i++) {
+ var pair = vars[i].split("="),
+ k = decodeURIComponent(pair[0]),
+ v, m;
+
+ if (!pair[1]) { continue };
+ v = decodeURIComponent(pair[1].replace(/\+/g, ' '));
+
+ // modified to parse multi-level parameters (e.g. "hi[there][dude]=whatsup" => hi: {there: {dude: "whatsup"}})
+ while (m = k.match(/([^&=]+)\[([^&=]+)\]$/)) {
+ var origV = v;
+ k = m[1];
+ v = {};
+
+ // If nested param ends in '][', then the regex above erroneously included half of a trailing '[]',
+ // which indicates the end-value is part of an array
+ if (m[2].substr(m[2].length-2) == '][') { // must use substr for IE to understand it
+ v[m[2].substr(0,m[2].length-2)] = [origV];
+ } else {
+ v[m[2]] = origV;
+ }
+ }
+
+ // If it is the first entry with this name
+ if (typeof hash[k] === "undefined") {
+ if (k.substr(k.length-2) != '[]') { // not end with []. cannot use negative index as IE doesn't understand it
+ hash[k] = v;
+ } else {
+ hash[k] = [v];
+ }
+ // If subsequent entry with this name and not array
+ } else if (typeof hash[k] === "string") {
+ hash[k] = v; // replace it
+ // modified to add support for objects
+ } else if (typeof hash[k] === "object") {
+ hash[k] = $.extend({}, hash[k], v);
+ // If subsequent entry with this name and is array
+ } else {
+ hash[k].push(v);
+ }
+ }
+ return hash;
+ },
+ refreshQueryString: function(urlString, data, settings) {
+ var _this = this,
+ queryString = urlString.split('?'),
+ path = queryString.shift(),
+ urlOptions;
+
+ urlOptions = this.deserialize(urlString);
+
+ // Loop through each dynatable param and update the URL with it
+ for (attr in settings.params) {
+ if (settings.params.hasOwnProperty(attr)) {
+ var label = settings.params[attr];
+ // Skip over parameters matching attributes for disabled features (i.e. leave them untouched),
+ // because if the feature is turned off, then parameter name is a coincidence and it's unrelated to dynatable.
+ if (
+ (!settings.features.sort && attr == "sorts") ||
+ (!settings.features.paginate && _this.anyMatch(attr, ["page", "perPage", "offset"], function(attr, param) { return attr == param; }))
+ ) {
+ continue;
+ }
+
+ // Delete page and offset from url params if on page 1 (default)
+ if ((attr === "page" || attr === "offset") && data["page"] === 1) {
+ if (urlOptions[label]) {
+ delete urlOptions[label];
+ }
+ continue;
+ }
+
+ // Delete perPage from url params if default perPage value
+ if (attr === "perPage" && data[label] == settings.dataset.perPageDefault) {
+ if (urlOptions[label]) {
+ delete urlOptions[label];
+ }
+ continue;
+ }
+
+ // For queries, we're going to handle each possible query parameter individually here instead of
+ // handling the entire queries object below, since we need to make sure that this is a query controlled by dynatable.
+ if (attr == "queries" && data[label]) {
+ var queries = settings.inputs.queries || [],
+ inputQueries = $.makeArray(queries.map(function() { return $(this).attr('name') }));
+ for (var i = 0, len = inputQueries.length; i < len; i++) {
+ var attr = inputQueries[i];
+ if (data[label][attr]) {
+ if (typeof urlOptions[label] === 'undefined') { urlOptions[label] = {}; }
+ urlOptions[label][attr] = data[label][attr];
+ } else {
+ delete urlOptions[label][attr];
+ }
+ }
+ continue;
+ }
+
+ // If we havne't returned true by now, then we actually want to update the parameter in the URL
+ if (data[label]) {
+ urlOptions[label] = data[label];
+ } else {
+ delete urlOptions[label];
+ }
+ }
+ }
+ return decodeURI($.param(urlOptions));
+ },
+ // Get array of keys from object
+ // see http://stackoverflow.com/questions/208016/how-to-list-the-properties-of-a-javascript-object/208020#208020
+ keysFromObject: function(obj){
+ var keys = [];
+ for (var key in obj){
+ keys.push(key);
+ }
+ return keys;
+ },
+ // Find an object in an array of objects by attributes.
+ // E.g. find object with {id: 'hi', name: 'there'} in an array of objects
+ findObjectInArray: function(array, objectAttr) {
+ var _this = this,
+ foundObject;
+ for (var i = 0, len = array.length; i < len; i++) {
+ var item = array[i];
+ // For each object in array, test to make sure all attributes in objectAttr match
+ if (_this.allMatch(item, objectAttr, function(item, key, value) { return item[key] == value; })) {
+ foundObject = item;
+ break;
+ }
+ }
+ return foundObject;
+ },
+ // Return true if supplied test function passes for ALL items in an array
+ allMatch: function(item, arrayOrObject, test) {
+ // start off with true result by default
+ var match = true,
+ isArray = $.isArray(arrayOrObject);
+ // Loop through all items in array
+ $.each(arrayOrObject, function(key, value) {
+ var result = isArray ? test(item, value) : test(item, key, value);
+ // If a single item tests false, go ahead and break the array by returning false
+ // and return false as result,
+ // otherwise, continue with next iteration in loop
+ // (if we make it through all iterations without overriding match with false,
+ // then we can return the true result we started with by default)
+ if (!result) { return match = false; }
+ });
+ return match;
+ },
+ // Return true if supplied test function passes for ANY items in an array
+ anyMatch: function(item, arrayOrObject, test) {
+ var match = false,
+ isArray = $.isArray(arrayOrObject);
+
+ $.each(arrayOrObject, function(key, value) {
+ var result = isArray ? test(item, value) : test(item, key, value);
+ if (result) {
+ // As soon as a match is found, set match to true, and return false to stop the `$.each` loop
+ match = true;
+ return false;
+ }
+ });
+ return match;
+ },
+ // Return true if two objects are equal
+ // (i.e. have the same attributes and attribute values)
+ objectsEqual: function(a, b) {
+ for (attr in a) {
+ if (a.hasOwnProperty(attr)) {
+ if (!b.hasOwnProperty(attr) || a[attr] !== b[attr]) {
+ return false;
+ }
+ }
+ }
+ for (attr in b) {
+ if (b.hasOwnProperty(attr) && !a.hasOwnProperty(attr)) {
+ return false;
+ }
+ }
+ return true;
+ },
+ // Taken from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/105074#105074
+ randomHash: function() {
+ return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
+ }
+ };
+
+ //-----------------------------------------------------------------
+ // Build the dynatable plugin
+ //-----------------------------------------------------------------
+
+ // Object.create support test, and fallback for browsers without it
+ if ( typeof Object.create !== "function" ) {
+ Object.create = function (o) {
+ function F() {}
+ F.prototype = o;
+ return new F();
+ };
+ }
+
+ //-----------------------------------------------------------------
+ // Global dynatable plugin setting defaults
+ //-----------------------------------------------------------------
+
+ $.dynatableSetup = function(options) {
+ defaults = mergeSettings(options);
+ };
+
+ // Create dynatable plugin based on a defined object
+ $.dynatable = function( object ) {
+ $.fn['dynatable'] = function( options ) {
+ return this.each(function() {
+ if ( ! $.data( this, 'dynatable' ) ) {
+ $.data( this, 'dynatable', Object.create(object).init(this, options) );
+ }
+ });
+ };
+ };
+
+ $.dynatable(dt);
+
+})(jQuery);
templates/admin.html
@@ -2,6 +2,7 @@
{% block css %}
<link rel="stylesheet" href="{{ static_url("ffu.css") }}">
+<link rel="stylesheet" href="{{ static_url("jquery.dynatable.css") }}">
{% end %}
{% block content %}
@@ -9,11 +10,40 @@
<div class="row">
<div class="col-xs-12">
<ul id="filelist" class="list-group">
- {% for item in items %}
- <li><a href="#">{{item}}</a></li>
+ {% if 'items' in globals() %}
+ {% for item in items %}
+ <li><a href="#">{{item}}</a></li>
+ {% end %}
{% end %}
</ul>
+ <table id="audit-table">
+ <thead>
+ <th data-dynatable-column="transferid">Transfer ID</th>
+ <th>Username</th>
+ <th>Error</th>
+ <th>Filenames</th>
+ <th>Warnings</th>
+ <th>Actions</th>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
</div>
</div>
</div><!-- /.container -->
{% end %}
+
+{% block js %}
+<script src="{{ static_url("jquery.dynatable.js") }}"></script>
+<script>
+ $("#audit-table").dynatable({
+ dataset : {
+ ajax : true,
+ ajaxUrl: '/admin',
+ ajaxOnLoad: true,
+ defaultColumnIdStyle: 'lowercase',
+ records: []
+ }
+ });
+</script>
+{% end %}
templates/login.html
@@ -12,7 +12,7 @@
<!--<label class="checkbox">
<input type="checkbox" value="remember-me"> Remember me
</label>-->
- {% if errormsg is not None %}
+ {% if 'errormsg' in globals() %}
<div class="alert-warning text-center bot-padding">{{ errormsg }}</div>
{% end %}
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
ffu.py
@@ -4,14 +4,14 @@ import tornado.web
#from login import login
#from soccermove import process_file_import
# Tornado
+from functools import wraps
+import time, random, json, os
WORKING_DIR = os.path.dirname(os.path.abspath(__file__))
COOKIE_SECRET = "F+ek4AWESWewGryPRbzq5aSHR1uRD0/ItZenLimCkGI="
STATIC_DIR = os.path.join(WORKING_DIR,"static")
TEMPLATE_DIR = os.path.join(WORKING_DIR,"templates")
AUDIT_DIR = os.path.join(WORKING_DIR,"audit")
-import time, random, json, os
-
class BaseHandler(tornado.web.RequestHandler):
def get_login_url(self):
return u"/login"
@@ -31,14 +31,6 @@ class BaseHandler(tornado.web.RequestHandler):
return None
def render(self, template, **kwargs):
- if hasattr(self, 'errormsg'):
- kwargs['errormsg'] = self.errormsg
- else:
- kwargs['errormsg'] = None
- if hasattr(self, 'items'):
- kwargs['items'] = self.items
- else:
- kwargs['items'] = []
kwargs['admin'] = self.get_admin_status()
super(BaseHandler, self).render(template, **kwargs)
@@ -50,6 +42,7 @@ class LoginHandler(BaseHandler):
username = self.get_argument("username", "")
password = self.get_argument("password", "")
#status, error = login(username, password)
+ error = "error"
status = ((username=="user" or username=="userb") and password=="pass")
if status:
#adminstatus = error #Yes, ghetto
@@ -58,9 +51,7 @@ class LoginHandler(BaseHandler):
self.set_admin_status(adminstatus)
self.redirect(self.get_argument("next",u"/"))
else:
- #self.errormsg = error
- self.errormsg = "error"
- self.render("login.html", errormsg=self.errormsg)
+ self.render("login.html", errormsg=error)
def set_current_user(self, user):
if user:
@@ -111,18 +102,30 @@ class UploadHandler(BaseHandler):
f.write(json.dumps(manifest))
f.close()
+def is_admin(method):
+ @wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if self.get_admin_status():
+ return method(self, *args, **kwargs)
+ else:
+ self.redirect(u"/")
+ return wrapper
+
class AdminHandler(BaseHandler):
@tornado.web.authenticated
+ @is_admin
def get(self):
- if self.get_admin_status():
- self.items = []
+ if self.request.headers.get('X-Requested-With') == "XMLHttpRequest":
+ items = []
for filename in os.listdir(AUDIT_DIR):
fout = open(os.path.join(AUDIT_DIR,filename), 'rb')
- self.items.append(json.loads(fout.read()))
+ items.append(json.loads(fout.read()))
fout.close()
- self.render("admin.html", items=self.items)
+ self.write(json.dumps(items))
+ self.set_header("Content-Type", "application/json; charset=UTF-8")
+ self.finish()
else:
- self.redirect(u"/")
+ self.render("admin.html")
handlers = [
(r"/", UploadHandler),