From 5b70170ec7e4fe463074049d5d0b5a8ea8607052 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 18 Feb 2012 09:46:26 +0000 Subject: [PATCH 01/24] [#40,view][s]: add support for CellRenderer as argument to DataTable and DataTableRow (fixes #40). --- src/view.js | 39 ++++++++++++++++++++++++++++++++------- test/view.test.js | 14 ++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/view.js b/src/view.js index 32f7cc9c..31418cdc 100644 --- a/src/view.js +++ b/src/view.js @@ -195,11 +195,15 @@ my.DataExplorer = Backbone.View.extend({ // Provides a tabular view on a Dataset. // // Initialize it with a recline.Dataset object. +// +// Additional options passed in second arguments. Options: +// +// * cellRenderer: function used to render individual cells. See DataTableRow for more. my.DataTable = Backbone.View.extend({ tagName: "div", className: "data-table-container", - initialize: function() { + initialize: function(modelEtc, options) { var self = this; this.el = $(this.el); _.bindAll(this, 'render'); @@ -208,6 +212,7 @@ my.DataTable = Backbone.View.extend({ this.model.currentDocuments.bind('remove', this.render); this.state = {}; this.hiddenFields = []; + this.options = options; }, events: { @@ -392,7 +397,9 @@ my.DataTable = Backbone.View.extend({ model: doc, el: tr, fields: self.fields, - }); + }, + self.options + ); newView.render(); }); this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0)); @@ -404,10 +411,25 @@ my.DataTable = Backbone.View.extend({ // // Since we want this to update in place it is up to creator to provider the element to attach to. // In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable. +// +// Additional options can be passed in a second hash argument. Options: +// +// * cellRenderer: function to render cells. Signature: function(value, +// field, doc) where value is the value of this cell, field is +// corresponding field object and document is the document object. Note +// that implementing functions can ignore arguments (e.g. +// function(value) would be a valid cellRenderer function). my.DataTableRow = Backbone.View.extend({ - initialize: function(options) { + initialize: function(initData, options) { _.bindAll(this, 'render'); - this._fields = options.fields; + this._fields = initData.fields; + if (options && options.cellRenderer) { + this._cellRenderer = options.cellRenderer; + } else { + this._cellRenderer = function(value) { + return value; + } + } this.el = $(this.el); this.model.bind('change', this.render); }, @@ -418,22 +440,25 @@ my.DataTableRow = Backbone.View.extend({ \
\   \ -
{{value}}
\ +
{{{value}}}
\
\ \ {{/cells}} \ ', events: { 'click .data-table-cell-edit': 'onEditClick', - // cell editor 'click .data-table-cell-editor .okButton': 'onEditorOK', 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' }, toTemplateJSON: function() { + var self = this; var doc = this.model; var cellData = this._fields.map(function(field) { - return {field: field.id, value: doc.get(field.id)} + return { + field: field.id, + value: self._cellRenderer(doc.get(field.id), field, doc) + } }) return { id: this.id, cells: cellData } }, diff --git a/test/view.test.js b/test/view.test.js index 3b9c62e5..f969160d 100644 --- a/test/view.test.js +++ b/test/view.test.js @@ -20,6 +20,20 @@ test('new DataTableRow View', function () { var tds = $el.find('td'); equal(tds.length, 3); equal($(tds[1]).attr('data-field'), 'a'); + + var view = new recline.View.DataTableRow({ + model: doc + , el: $el + , fields: new recline.Model.FieldList([{id: 'a'}, {id: 'b'}]) + }, + { + cellRenderer: function(value, field) { + return '' + value + ''; + } + }); + view.render(); + var tds = $el.find('td .data-table-cell-value'); + equal($(tds[0]).html(), '1', 'Checking cellRenderer works'); }); })(this.jQuery); From 7cd6b0b2845120f7dd0db6c514addc77d9ac8bf2 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Wed, 22 Feb 2012 20:35:41 +0000 Subject: [PATCH 02/24] [#47,package.json][s]: add basic commonjs package.json, fixes #47. --- package.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 00000000..be14f8df --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name" : "recline", + "description" : "Data explorer library and app in pure Javascript", + "url" : "http://okfnlabs.org/recline", + "keywords" : ["data", "explorer", "grid", "table", "library", "app"], + "author" : "Rufus Pollock and Max Ogden ", + "contributors" : [], + "dependencies" : { + "jquery" : ">=1.6", + "underscore" : ">=1.0", + "backbone" : ">=0.5", + "jquery.mustache" : "" + }, + "lib" : "src", + "main" : "recline.js", + "version" : "0.3a" +} From 1abf2769bd6d7161552c1f26e40744497d463e88 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Wed, 22 Feb 2012 21:06:58 +0000 Subject: [PATCH 03/24] [#48,view/grid][xs]: split out DataTable (soon to be Grid or DataGrid) into its own file. --- demo/index.html | 1 + src/view-grid.js | 325 +++++++++++++++++++++++++++++++++++++++++++++++ src/view.js | 320 ---------------------------------------------- test/index.html | 1 + 4 files changed, 327 insertions(+), 320 deletions(-) create mode 100644 src/view-grid.js diff --git a/demo/index.html b/demo/index.html index c941752a..fdbc8d2b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -28,6 +28,7 @@ + diff --git a/src/view-grid.js b/src/view-grid.js new file mode 100644 index 00000000..80d9820b --- /dev/null +++ b/src/view-grid.js @@ -0,0 +1,325 @@ +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { +// ## DataTable +// +// Provides a tabular view on a Dataset. +// +// Initialize it with a recline.Dataset object. +// +// Additional options passed in second arguments. Options: +// +// * cellRenderer: function used to render individual cells. See DataTableRow for more. +my.DataTable = Backbone.View.extend({ + tagName: "div", + className: "data-table-container", + + initialize: function(modelEtc, options) { + var self = this; + this.el = $(this.el); + _.bindAll(this, 'render'); + this.model.currentDocuments.bind('add', this.render); + this.model.currentDocuments.bind('reset', this.render); + this.model.currentDocuments.bind('remove', this.render); + this.state = {}; + this.hiddenFields = []; + this.options = options; + }, + + events: { + 'click .column-header-menu': 'onColumnHeaderClick' + , 'click .row-header-menu': 'onRowHeaderClick' + , 'click .root-header-menu': 'onRootHeaderClick' + , 'click .data-table-menu li a': 'onMenuClick' + }, + + // TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). + // showDialog: function(template, data) { + // if (!data) data = {}; + // util.show('dialog'); + // util.render(template, 'dialog-content', data); + // util.observeExit($('.dialog-content'), function() { + // util.hide('dialog'); + // }) + // $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + // }, + + + // ====================================================== + // Column and row menus + + onColumnHeaderClick: function(e) { + this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field'); + util.position('data-table-menu', e); + util.render('columnActions', 'data-table-menu'); + }, + + onRowHeaderClick: function(e) { + this.state.currentRow = $(e.target).parents('tr:first').attr('data-id'); + util.position('data-table-menu', e); + util.render('rowActions', 'data-table-menu'); + }, + + onRootHeaderClick: function(e) { + util.position('data-table-menu', e); + util.render('rootActions', 'data-table-menu', {'columns': this.hiddenFields}); + }, + + onMenuClick: function(e) { + var self = this; + e.preventDefault(); + var actions = { + bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) }, + transform: function() { self.showTransformDialog('transform') }, + sortAsc: function() { self.setColumnSort('asc') }, + sortDesc: function() { self.setColumnSort('desc') }, + hideColumn: function() { self.hideColumn() }, + showColumn: function() { self.showColumn(e) }, + // TODO: Delete or re-implement ... + csv: function() { window.location.href = app.csvUrl }, + json: function() { window.location.href = "_rewrite/api/json" }, + urlImport: function() { showDialog('urlImport') }, + pasteImport: function() { showDialog('pasteImport') }, + uploadImport: function() { showDialog('uploadImport') }, + // END TODO + deleteColumn: function() { + var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents."; + // TODO: + alert('This function needs to be re-implemented'); + return; + if (confirm(msg)) costco.deleteColumn(self.state.currentColumn); + }, + deleteRow: function() { + var doc = _.find(self.model.currentDocuments.models, function(doc) { + // important this is == as the currentRow will be string (as comes + // from DOM) while id may be int + return doc.id == self.state.currentRow + }); + doc.destroy().then(function() { + self.model.currentDocuments.remove(doc); + my.notify("Row deleted successfully"); + }) + .fail(function(err) { + my.notify("Errorz! " + err) + }) + } + } + util.hide('data-table-menu'); + actions[$(e.target).attr('data-action')](); + }, + + showTransformColumnDialog: function() { + var $el = $('.dialog-content'); + util.show('dialog'); + var view = new my.ColumnTransform({ + model: this.model + }); + view.state = this.state; + view.render(); + $el.empty(); + $el.append(view.el); + util.observeExit($el, function() { + util.hide('dialog'); + }) + $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + }, + + showTransformDialog: function() { + var $el = $('.dialog-content'); + util.show('dialog'); + var view = new recline.View.DataTransform({ + }); + view.render(); + $el.empty(); + $el.append(view.el); + util.observeExit($el, function() { + util.hide('dialog'); + }) + $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + }, + + setColumnSort: function(order) { + this.model.query({ + sort: [ + [this.state.currentColumn, order] + ] + }); + }, + + hideColumn: function() { + this.hiddenFields.push(this.state.currentColumn); + this.render(); + }, + + showColumn: function(e) { + this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column')); + this.render(); + }, + + // ====================================================== + // #### Templating + template: ' \ + \ +
    \ + \ + \ + \ + {{#notEmpty}} \ + \ + {{/notEmpty}} \ + {{#fields}} \ + \ + {{/fields}} \ + \ + \ + \ +
    \ +
    \ + \ + \ +
    \ +
    \ +
    \ + \ + {{label}} \ +
    \ + \ +
    \ + ', + + toTemplateJSON: function() { + var modelData = this.model.toJSON() + modelData.notEmpty = ( this.fields.length > 0 ) + // TODO: move this sort of thing into a toTemplateJSON method on Dataset? + modelData.fields = _.map(this.fields, function(field) { return field.toJSON() }); + return modelData; + }, + render: function() { + var self = this; + this.fields = this.model.fields.filter(function(field) { + return _.indexOf(self.hiddenFields, field.id) == -1; + }); + var htmls = $.mustache(this.template, this.toTemplateJSON()); + this.el.html(htmls); + this.model.currentDocuments.forEach(function(doc) { + var tr = $(''); + self.el.find('tbody').append(tr); + var newView = new my.DataTableRow({ + model: doc, + el: tr, + fields: self.fields, + }, + self.options + ); + newView.render(); + }); + this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0)); + return this; + } +}); + +// ## DataTableRow View for rendering an individual document. +// +// Since we want this to update in place it is up to creator to provider the element to attach to. +// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable. +// +// Additional options can be passed in a second hash argument. Options: +// +// * cellRenderer: function to render cells. Signature: function(value, +// field, doc) where value is the value of this cell, field is +// corresponding field object and document is the document object. Note +// that implementing functions can ignore arguments (e.g. +// function(value) would be a valid cellRenderer function). +my.DataTableRow = Backbone.View.extend({ + initialize: function(initData, options) { + _.bindAll(this, 'render'); + this._fields = initData.fields; + if (options && options.cellRenderer) { + this._cellRenderer = options.cellRenderer; + } else { + this._cellRenderer = function(value) { + return value; + } + } + this.el = $(this.el); + this.model.bind('change', this.render); + }, + + template: ' \ + \ + {{#cells}} \ + \ +
    \ +   \ +
    {{{value}}}
    \ +
    \ + \ + {{/cells}} \ + ', + events: { + 'click .data-table-cell-edit': 'onEditClick', + 'click .data-table-cell-editor .okButton': 'onEditorOK', + 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' + }, + + toTemplateJSON: function() { + var self = this; + var doc = this.model; + var cellData = this._fields.map(function(field) { + return { + field: field.id, + value: self._cellRenderer(doc.get(field.id), field, doc) + } + }) + return { id: this.id, cells: cellData } + }, + + render: function() { + this.el.attr('data-id', this.model.id); + var html = $.mustache(this.template, this.toTemplateJSON()); + $(this.el).html(html); + return this; + }, + + // Cell Editor + // =========== + + onEditClick: function(e) { + var editing = this.el.find('.data-table-cell-editor-editor'); + if (editing.length > 0) { + editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); + } + $(e.target).addClass("hidden"); + var cell = $(e.target).siblings('.data-table-cell-value'); + cell.data("previousContents", cell.text()); + util.render('cellEditor', cell, {value: cell.text()}); + }, + + onEditorOK: function(e) { + var cell = $(e.target); + var rowId = cell.parents('tr').attr('data-id'); + var field = cell.parents('td').attr('data-field'); + var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val(); + var newData = {}; + newData[field] = newValue; + this.model.set(newData); + my.notify("Updating row...", {loader: true}); + this.model.save().then(function(response) { + my.notify("Row updated successfully", {category: 'success'}); + }) + .fail(function() { + my.notify('Error saving row', { + category: 'error', + persist: true + }); + }); + }, + + onEditorCancel: function(e) { + var cell = $(e.target).parents('.data-table-cell-value'); + cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); + } +}); + +})(jQuery, recline.View); diff --git a/src/view.js b/src/view.js index 31418cdc..bb35adc9 100644 --- a/src/view.js +++ b/src/view.js @@ -190,326 +190,6 @@ my.DataExplorer = Backbone.View.extend({ } }); -// ## DataTable -// -// Provides a tabular view on a Dataset. -// -// Initialize it with a recline.Dataset object. -// -// Additional options passed in second arguments. Options: -// -// * cellRenderer: function used to render individual cells. See DataTableRow for more. -my.DataTable = Backbone.View.extend({ - tagName: "div", - className: "data-table-container", - - initialize: function(modelEtc, options) { - var self = this; - this.el = $(this.el); - _.bindAll(this, 'render'); - this.model.currentDocuments.bind('add', this.render); - this.model.currentDocuments.bind('reset', this.render); - this.model.currentDocuments.bind('remove', this.render); - this.state = {}; - this.hiddenFields = []; - this.options = options; - }, - - events: { - 'click .column-header-menu': 'onColumnHeaderClick' - , 'click .row-header-menu': 'onRowHeaderClick' - , 'click .root-header-menu': 'onRootHeaderClick' - , 'click .data-table-menu li a': 'onMenuClick' - }, - - // TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). - // showDialog: function(template, data) { - // if (!data) data = {}; - // util.show('dialog'); - // util.render(template, 'dialog-content', data); - // util.observeExit($('.dialog-content'), function() { - // util.hide('dialog'); - // }) - // $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); - // }, - - - // ====================================================== - // Column and row menus - - onColumnHeaderClick: function(e) { - this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field'); - util.position('data-table-menu', e); - util.render('columnActions', 'data-table-menu'); - }, - - onRowHeaderClick: function(e) { - this.state.currentRow = $(e.target).parents('tr:first').attr('data-id'); - util.position('data-table-menu', e); - util.render('rowActions', 'data-table-menu'); - }, - - onRootHeaderClick: function(e) { - util.position('data-table-menu', e); - util.render('rootActions', 'data-table-menu', {'columns': this.hiddenFields}); - }, - - onMenuClick: function(e) { - var self = this; - e.preventDefault(); - var actions = { - bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) }, - transform: function() { self.showTransformDialog('transform') }, - sortAsc: function() { self.setColumnSort('asc') }, - sortDesc: function() { self.setColumnSort('desc') }, - hideColumn: function() { self.hideColumn() }, - showColumn: function() { self.showColumn(e) }, - // TODO: Delete or re-implement ... - csv: function() { window.location.href = app.csvUrl }, - json: function() { window.location.href = "_rewrite/api/json" }, - urlImport: function() { showDialog('urlImport') }, - pasteImport: function() { showDialog('pasteImport') }, - uploadImport: function() { showDialog('uploadImport') }, - // END TODO - deleteColumn: function() { - var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents."; - // TODO: - alert('This function needs to be re-implemented'); - return; - if (confirm(msg)) costco.deleteColumn(self.state.currentColumn); - }, - deleteRow: function() { - var doc = _.find(self.model.currentDocuments.models, function(doc) { - // important this is == as the currentRow will be string (as comes - // from DOM) while id may be int - return doc.id == self.state.currentRow - }); - doc.destroy().then(function() { - self.model.currentDocuments.remove(doc); - my.notify("Row deleted successfully"); - }) - .fail(function(err) { - my.notify("Errorz! " + err) - }) - } - } - util.hide('data-table-menu'); - actions[$(e.target).attr('data-action')](); - }, - - showTransformColumnDialog: function() { - var $el = $('.dialog-content'); - util.show('dialog'); - var view = new my.ColumnTransform({ - model: this.model - }); - view.state = this.state; - view.render(); - $el.empty(); - $el.append(view.el); - util.observeExit($el, function() { - util.hide('dialog'); - }) - $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); - }, - - showTransformDialog: function() { - var $el = $('.dialog-content'); - util.show('dialog'); - var view = new recline.View.DataTransform({ - }); - view.render(); - $el.empty(); - $el.append(view.el); - util.observeExit($el, function() { - util.hide('dialog'); - }) - $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); - }, - - setColumnSort: function(order) { - this.model.query({ - sort: [ - [this.state.currentColumn, order] - ] - }); - }, - - hideColumn: function() { - this.hiddenFields.push(this.state.currentColumn); - this.render(); - }, - - showColumn: function(e) { - this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column')); - this.render(); - }, - - // ====================================================== - // #### Templating - template: ' \ - \ -
      \ - \ - \ - \ - {{#notEmpty}} \ - \ - {{/notEmpty}} \ - {{#fields}} \ - \ - {{/fields}} \ - \ - \ - \ -
      \ -
      \ - \ - \ -
      \ -
      \ -
      \ - \ - {{label}} \ -
      \ - \ -
      \ - ', - - toTemplateJSON: function() { - var modelData = this.model.toJSON() - modelData.notEmpty = ( this.fields.length > 0 ) - // TODO: move this sort of thing into a toTemplateJSON method on Dataset? - modelData.fields = _.map(this.fields, function(field) { return field.toJSON() }); - return modelData; - }, - render: function() { - var self = this; - this.fields = this.model.fields.filter(function(field) { - return _.indexOf(self.hiddenFields, field.id) == -1; - }); - var htmls = $.mustache(this.template, this.toTemplateJSON()); - this.el.html(htmls); - this.model.currentDocuments.forEach(function(doc) { - var tr = $(''); - self.el.find('tbody').append(tr); - var newView = new my.DataTableRow({ - model: doc, - el: tr, - fields: self.fields, - }, - self.options - ); - newView.render(); - }); - this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0)); - return this; - } -}); - -// ## DataTableRow View for rendering an individual document. -// -// Since we want this to update in place it is up to creator to provider the element to attach to. -// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable. -// -// Additional options can be passed in a second hash argument. Options: -// -// * cellRenderer: function to render cells. Signature: function(value, -// field, doc) where value is the value of this cell, field is -// corresponding field object and document is the document object. Note -// that implementing functions can ignore arguments (e.g. -// function(value) would be a valid cellRenderer function). -my.DataTableRow = Backbone.View.extend({ - initialize: function(initData, options) { - _.bindAll(this, 'render'); - this._fields = initData.fields; - if (options && options.cellRenderer) { - this._cellRenderer = options.cellRenderer; - } else { - this._cellRenderer = function(value) { - return value; - } - } - this.el = $(this.el); - this.model.bind('change', this.render); - }, - - template: ' \ - \ - {{#cells}} \ - \ -
      \ -   \ -
      {{{value}}}
      \ -
      \ - \ - {{/cells}} \ - ', - events: { - 'click .data-table-cell-edit': 'onEditClick', - 'click .data-table-cell-editor .okButton': 'onEditorOK', - 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' - }, - - toTemplateJSON: function() { - var self = this; - var doc = this.model; - var cellData = this._fields.map(function(field) { - return { - field: field.id, - value: self._cellRenderer(doc.get(field.id), field, doc) - } - }) - return { id: this.id, cells: cellData } - }, - - render: function() { - this.el.attr('data-id', this.model.id); - var html = $.mustache(this.template, this.toTemplateJSON()); - $(this.el).html(html); - return this; - }, - - // Cell Editor - // =========== - - onEditClick: function(e) { - var editing = this.el.find('.data-table-cell-editor-editor'); - if (editing.length > 0) { - editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); - } - $(e.target).addClass("hidden"); - var cell = $(e.target).siblings('.data-table-cell-value'); - cell.data("previousContents", cell.text()); - util.render('cellEditor', cell, {value: cell.text()}); - }, - - onEditorOK: function(e) { - var cell = $(e.target); - var rowId = cell.parents('tr').attr('data-id'); - var field = cell.parents('td').attr('data-field'); - var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val(); - var newData = {}; - newData[field] = newValue; - this.model.set(newData); - my.notify("Updating row...", {loader: true}); - this.model.save().then(function(response) { - my.notify("Row updated successfully", {category: 'success'}); - }) - .fail(function() { - my.notify('Error saving row', { - category: 'error', - persist: true - }); - }); - }, - - onEditorCancel: function(e) { - var cell = $(e.target).parents('.data-table-cell-value'); - cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); - } -}); - /* ========================================================== */ // ## Miscellaneous Utilities diff --git a/test/index.html b/test/index.html index cd87c9af..a00c9f81 100644 --- a/test/index.html +++ b/test/index.html @@ -21,6 +21,7 @@ + From 1fd337e1d4e0bb5877fb9788fabc8c964f0eba39 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Wed, 22 Feb 2012 21:18:30 +0000 Subject: [PATCH 04/24] [#48,refactor][s]: rename DataTable to DataGrid - fixes #48. --- demo/js/app.js | 2 +- index.html | 8 ++++---- src/view-grid.js | 14 +++++++------- src/view.js | 6 +++--- test/view.test.js | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/demo/js/app.js b/demo/js/app.js index d0360492..29bb3da1 100755 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -6,7 +6,7 @@ $(function() { { id: 'grid', label: 'Grid', - view: new recline.View.DataTable({ + view: new recline.View.DataGrid({ model: dataset }) }, diff --git a/index.html b/index.html index a69f2f88..1aaa14a9 100644 --- a/index.html +++ b/index.html @@ -79,9 +79,9 @@
    • Pure javascript (no Flash) and designed for integration -- so it is easy to embed in other sites and applications
    • Open-source
    • -
    • Built on Backbone - so - robust design and extremely easy to exend
    • +
    • Built on the simple but powerful Backbone giving a + clean and robust design which is easy to extend
    • Properly designed model with clean separation of data and presentation
    • Componentized design means you use only what you need
    • @@ -114,7 +114,7 @@

      There are then various Views (you can easily write your own). Each view holds a pointer to a Dataset:

      • DataExplorer: the parent view which manages the overall app and sets up sub views.
      • -
      • DataTable: the data grid / table view.
      • +
      • DataGrid: the data grid view.
      • FlotGraph: a simple graphing view using Flot.
      diff --git a/src/view-grid.js b/src/view-grid.js index 80d9820b..feb02b85 100644 --- a/src/view-grid.js +++ b/src/view-grid.js @@ -2,7 +2,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { -// ## DataTable +// ## DataGrid // // Provides a tabular view on a Dataset. // @@ -10,8 +10,8 @@ this.recline.View = this.recline.View || {}; // // Additional options passed in second arguments. Options: // -// * cellRenderer: function used to render individual cells. See DataTableRow for more. -my.DataTable = Backbone.View.extend({ +// * cellRenderer: function used to render individual cells. See DataGridRow for more. +my.DataGrid = Backbone.View.extend({ tagName: "div", className: "data-table-container", @@ -205,7 +205,7 @@ my.DataTable = Backbone.View.extend({ this.model.currentDocuments.forEach(function(doc) { var tr = $(''); self.el.find('tbody').append(tr); - var newView = new my.DataTableRow({ + var newView = new my.DataGridRow({ model: doc, el: tr, fields: self.fields, @@ -219,10 +219,10 @@ my.DataTable = Backbone.View.extend({ } }); -// ## DataTableRow View for rendering an individual document. +// ## DataGridRow View for rendering an individual document. // // Since we want this to update in place it is up to creator to provider the element to attach to. -// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable. +// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataGrid. // // Additional options can be passed in a second hash argument. Options: // @@ -231,7 +231,7 @@ my.DataTable = Backbone.View.extend({ // corresponding field object and document is the document object. Note // that implementing functions can ignore arguments (e.g. // function(value) would be a valid cellRenderer function). -my.DataTableRow = Backbone.View.extend({ +my.DataGridRow = Backbone.View.extend({ initialize: function(initData, options) { _.bindAll(this, 'render'); this._fields = initData.fields; diff --git a/src/view.js b/src/view.js index bb35adc9..503fa47c 100644 --- a/src/view.js +++ b/src/view.js @@ -23,14 +23,14 @@ this.recline.View = this.recline.View || {}; // // **views**: (optional) the views (Grid, Graph etc) for DataExplorer to // show. This is an array of view hashes. If not provided -// just initialize a DataTable with id 'grid'. Example: +// just initialize a DataGrid with id 'grid'. Example: // //
       // var views = [
       //   {
       //     id: 'grid', // used for routing
       //     label: 'Grid', // used for view switcher
      -//     view: new recline.View.DataTable({
      +//     view: new recline.View.DataGrid({
       //       model: dataset
       //     })
       //   },
      @@ -101,7 +101,7 @@ my.DataExplorer = Backbone.View.extend({
             this.pageViews = [{
               id: 'grid',
               label: 'Grid',
      -        view: new my.DataTable({
      +        view: new my.DataGrid({
                   model: this.model
                 })
             }];
      diff --git a/test/view.test.js b/test/view.test.js
      index f969160d..05adb60e 100644
      --- a/test/view.test.js
      +++ b/test/view.test.js
      @@ -2,7 +2,7 @@
       
       module("View");
       
      -test('new DataTableRow View', function () {
      +test('new DataGridRow View', function () {
         var $el = $('');
         $('.fixtures .test-datatable').append($el);
         var doc = new recline.Model.Document({
      @@ -10,7 +10,7 @@ test('new DataTableRow View', function () {
           'b': '2',
           'a': '1'
           });
      -  var view = new recline.View.DataTableRow({
      +  var view = new recline.View.DataGridRow({
           model: doc
           , el: $el
           , fields: new recline.Model.FieldList([{id: 'a'}, {id: 'b'}])
      @@ -21,7 +21,7 @@ test('new DataTableRow View', function () {
         equal(tds.length, 3);
         equal($(tds[1]).attr('data-field'), 'a');
       
      -  var view = new recline.View.DataTableRow({
      +  var view = new recline.View.DataGridRow({
             model: doc
             , el: $el
             , fields: new recline.Model.FieldList([{id: 'a'}, {id: 'b'}])
      
      From 12bb498d52c38157fa5252a17233d8e40652c48c Mon Sep 17 00:00:00 2001
      From: Rufus Pollock 
      Date: Wed, 22 Feb 2012 22:02:38 +0000
      Subject: [PATCH 05/24] [#53,view/query][s]: start on a query editor by
       factoring out current size/offset form out of DataExplorer into separate
       QueryEditor view.
      
      * This is also necessary prep for #27 (pagination and offset support)
      ---
       src/view.js | 59 +++++++++++++++++++++++++++++++++--------------------
       1 file changed, 37 insertions(+), 22 deletions(-)
      
      diff --git a/src/view.js b/src/view.js
      index 503fa47c..ef37a2cc 100644
      --- a/src/view.js
      +++ b/src/view.js
      @@ -46,7 +46,6 @@ this.recline.View = this.recline.View || {};
       //
       // **config**: Config options like:
       //
      -//   * displayCount: how many documents to display initially (default: 10)
       //   * readOnly: true/false (default: false) value indicating whether to
       //     operate in read-only mode (hiding all editing options).
       //
      @@ -63,11 +62,6 @@ my.DataExplorer = Backbone.View.extend({
               
    • {{label}} \ {{/views}} \ \ - \ \
      \ \ @@ -79,16 +73,11 @@ my.DataExplorer = Backbone.View.extend({ \ ', - events: { - 'submit form.display-count': 'onDisplayCountUpdate' - }, - initialize: function(options) { var self = this; this.el = $(this.el); this.config = _.extend({ - displayCount: 50 - , readOnly: false + readOnly: false }, options.config); if (this.config.readOnly) { @@ -124,13 +113,11 @@ my.DataExplorer = Backbone.View.extend({ }); }, + // TODO: listen for being query and end query events on the dataset ... + // (This is no longer called by anything ...) query: function() { - this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val()); - var queryObj = { - size: this.config.displayCount - }; my.notify('Loading data', {loader: true}); - this.model.query(queryObj) + this.model.query() .done(function() { my.clearNotifications(); my.notify('Data loaded', {category: 'success'}); @@ -141,11 +128,6 @@ my.DataExplorer = Backbone.View.extend({ }); }, - onDisplayCountUpdate: function(e) { - e.preventDefault(); - this.query(); - }, - setReadOnly: function() { this.el.addClass('read-only'); }, @@ -160,6 +142,10 @@ my.DataExplorer = Backbone.View.extend({ _.each(this.pageViews, function(view, pageName) { $dataViewContainer.append(view.view.el) }); + var queryEditor = new my.QueryEditor({ + model: this.model.queryState + }); + this.el.find('.header').append(queryEditor.el); }, setupRouting: function() { @@ -191,6 +177,35 @@ my.DataExplorer = Backbone.View.extend({ }); +my.QueryEditor = Backbone.View.extend({ + className: 'recline-query-editor', + template: ' \ +
      \ + Showing 0 to of {{docCount}} \ +
      \ + ', + + events: { + 'submit form.display-count': 'onFormSubmit' + }, + + initialize: function() { + this.el = $(this.el); + this.render(); + }, + onFormSubmit: function(e) { + e.preventDefault(); + var newSize = parseInt(this.el.find('input[name="displayCount"]').val()); + this.model.set({size: newSize}); + }, + render: function() { + var tmplData = this.model.toJSON(); + var templated = $.mustache(this.template, tmplData); + this.el.html(templated); + } +}); + + /* ========================================================== */ // ## Miscellaneous Utilities From f6dc590b3cace2c7710e450c2d227026dbeeef26 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 24 Feb 2012 09:07:59 +0000 Subject: [PATCH 06/24] [view/flot-graph,bugfix][xs]: fix exception on data change when flot not visible. --- src/view-flot-graph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view-flot-graph.js b/src/view-flot-graph.js index a27179ba..5bd1afe5 100644 --- a/src/view-flot-graph.js +++ b/src/view-flot-graph.js @@ -137,7 +137,7 @@ my.FlotGraph = Backbone.View.extend({ // Uncaught Invalid dimensions for plot, width = 0, height = 0 // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]); - if (!this.plot && (!areWeVisible || this.model.currentDocuments.length == 0)) { + if ((!areWeVisible || this.model.currentDocuments.length == 0)) { return } // create this.plot and cache it From 3c9bd368daba2d428d5579fd083a3e02d55d0aae Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 24 Feb 2012 09:10:44 +0000 Subject: [PATCH 07/24] [addendum,css][xs]: css that should have been in penultimate commit 12bb498d52c38157fa5252a17233d8e40652c48c. --- css/data-explorer.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/css/data-explorer.css b/css/data-explorer.css index 77aacfd6..2e62d6ca 100644 --- a/css/data-explorer.css +++ b/css/data-explorer.css @@ -12,16 +12,16 @@ padding-left: 0; } -.header .pagination { +.header .recline-query-editor { float: right; margin: 4px; } -.header .pagination label { +.header .recline-query-editor label { float: none; } -.header .pagination input { +.header .recline-query-editor input { width: 30px; } From aaa01b21913bd4d05f22d404415a91270ff0fc5b Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 24 Feb 2012 09:11:17 +0000 Subject: [PATCH 08/24] [refactor,model,view][s]: add explicit query start, done and fail events for dataset and use them in DataExplorer to generate notifications. --- src/model.js | 3 +++ src/view.js | 29 +++++++++++++---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/model.js b/src/model.js index 932902d9..853febd2 100644 --- a/src/model.js +++ b/src/model.js @@ -39,6 +39,7 @@ my.Dataset = Backbone.Model.extend({ // Resulting DocumentList are used to reset this.currentDocuments and are // also returned. query: function(queryObj) { + this.trigger('query:start'); var self = this; this.queryState.set(queryObj, {silent: true}); var dfd = $.Deferred(); @@ -50,9 +51,11 @@ my.Dataset = Backbone.Model.extend({ return _doc; }); self.currentDocuments.reset(docs); + self.trigger('query:done'); dfd.resolve(self.currentDocuments); }) .fail(function(arguments) { + self.trigger('query:fail', arguments); dfd.reject(arguments); }); return dfd.promise(); diff --git a/src/view.js b/src/view.js index ef37a2cc..79a57b85 100644 --- a/src/view.js +++ b/src/view.js @@ -101,33 +101,30 @@ my.DataExplorer = Backbone.View.extend({ this.router = new Backbone.Router(); this.setupRouting(); + this.model.bind('query:start', function(eventName) { + my.notify('Loading data', {loader: true}); + }); + this.model.bind('query:done', function(eventName) { + my.clearNotifications(); + my.notify('Data loaded', {category: 'success'}); + }); + this.model.bind('query:fail', function(eventName, error) { + my.clearNotifications(); + my.notify(error.message, {category: 'error', persist: true}); + }); + // retrieve basic data like fields etc // note this.model and dataset returned are the same this.model.fetch() .done(function(dataset) { self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - self.query(); + self.model.query(); }) .fail(function(error) { my.notify(error.message, {category: 'error', persist: true}); }); }, - // TODO: listen for being query and end query events on the dataset ... - // (This is no longer called by anything ...) - query: function() { - my.notify('Loading data', {loader: true}); - this.model.query() - .done(function() { - my.clearNotifications(); - my.notify('Data loaded', {category: 'success'}); - }) - .fail(function(error) { - my.clearNotifications(); - my.notify(error.message, {category: 'error', persist: true}); - }); - }, - setReadOnly: function() { this.el.addClass('read-only'); }, From e32d541fc7e9456e97da44218899b332d24d867b Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 24 Feb 2012 09:29:17 +0000 Subject: [PATCH 09/24] [#53,#27,view/query][s]: support for offset in query editor. --- src/view.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/view.js b/src/view.js index 79a57b85..26f4d86a 100644 --- a/src/view.js +++ b/src/view.js @@ -177,13 +177,14 @@ my.DataExplorer = Backbone.View.extend({ my.QueryEditor = Backbone.View.extend({ className: 'recline-query-editor', template: ' \ -
      \ - Showing 0 to of {{docCount}} \ + \ + Showing starting at of {{docCount}} \ + \
      \ ', events: { - 'submit form.display-count': 'onFormSubmit' + 'submit form': 'onFormSubmit' }, initialize: function() { @@ -192,8 +193,9 @@ my.QueryEditor = Backbone.View.extend({ }, onFormSubmit: function(e) { e.preventDefault(); - var newSize = parseInt(this.el.find('input[name="displayCount"]').val()); - this.model.set({size: newSize}); + var newSize = parseInt(this.el.find('input[name="size"]').val()); + var newOffset = parseInt(this.el.find('input[name="offset"]').val()); + this.model.set({size: newSize, offset: newOffset}); }, render: function() { var tmplData = this.model.toJSON(); From 0a17c428cdcc1a1276794b1abeb7e908d29e9bbe Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 24 Feb 2012 10:00:00 +0000 Subject: [PATCH 10/24] [#53,#27,pagination][s]: pagination support (fixes #27). * Also refactor to have doc count (which is dataset info) outside of query editor. --- css/data-explorer.css | 10 ++++++++++ src/view.js | 32 ++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/css/data-explorer.css b/css/data-explorer.css index 2e62d6ca..0844a9de 100644 --- a/css/data-explorer.css +++ b/css/data-explorer.css @@ -12,6 +12,12 @@ padding-left: 0; } +.header .recline-results-info { + line-height: 28px; + margin-left: 20px; + display: inline; +} + .header .recline-query-editor { float: right; margin: 4px; @@ -25,6 +31,10 @@ width: 30px; } +.header .recline-query-editor .pagination a { + line-height: 28px; +} + .data-view-container { display: block; clear: both; diff --git a/src/view.js b/src/view.js index 26f4d86a..ef5d7f99 100644 --- a/src/view.js +++ b/src/view.js @@ -62,6 +62,9 @@ my.DataExplorer = Backbone.View.extend({
    • {{label}} \ {{/views}} \ \ +
      \ + Results found {{docCount}} \ +
      \ \
      \ \ @@ -106,6 +109,7 @@ my.DataExplorer = Backbone.View.extend({ }); this.model.bind('query:done', function(eventName) { my.clearNotifications(); + self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); my.notify('Data loaded', {category: 'success'}); }); this.model.bind('query:fail', function(eventName, error) { @@ -178,27 +182,47 @@ my.QueryEditor = Backbone.View.extend({ className: 'recline-query-editor', template: ' \
      \ - Showing starting at of {{docCount}} \ - \ + \ + \
      \ ', events: { - 'submit form': 'onFormSubmit' + 'submit form': 'onFormSubmit', + 'click .action-pagination-update': 'onPaginationUpdate' }, initialize: function() { + _.bindAll(this, 'render'); this.el = $(this.el); + this.model.bind('change', this.render); this.render(); }, onFormSubmit: function(e) { e.preventDefault(); - var newSize = parseInt(this.el.find('input[name="size"]').val()); var newOffset = parseInt(this.el.find('input[name="offset"]').val()); + var newSize = parseInt(this.el.find('input[name="to"]').val()) - newOffset; this.model.set({size: newSize, offset: newOffset}); }, + onPaginationUpdate: function(e) { + e.preventDefault(); + var $el = $(e.target); + if ($el.parent().hasClass('prev')) { + var newOffset = this.model.get('offset') - Math.max(0, this.model.get('size')); + } else { + var newOffset = this.model.get('offset') + this.model.get('size'); + } + this.model.set({offset: newOffset}); + }, render: function() { var tmplData = this.model.toJSON(); + tmplData.to = this.model.get('offset') + this.model.get('size'); var templated = $.mustache(this.template, tmplData); this.el.html(templated); } From a470e66ff3d8fd1166ae9d55abcf28f9a74b6478 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 24 Feb 2012 10:08:19 +0000 Subject: [PATCH 11/24] [#53,view/query][s]: add free text query input to query editor (though backend does not yet use it). --- css/data-explorer.css | 12 +++++++++++- src/view.js | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/css/data-explorer.css b/css/data-explorer.css index 0844a9de..ea5ba775 100644 --- a/css/data-explorer.css +++ b/css/data-explorer.css @@ -27,7 +27,17 @@ float: none; } -.header .recline-query-editor input { +.header .recline-query-editor label { + float: none; +} + +.header .recline-query-editor input.text-query { + float: left; + margin-top: 1px; + margin-right: 5px; +} + +.header .recline-query-editor .pagination input { width: 30px; } diff --git a/src/view.js b/src/view.js index ef5d7f99..c3139645 100644 --- a/src/view.js +++ b/src/view.js @@ -182,6 +182,7 @@ my.QueryEditor = Backbone.View.extend({ className: 'recline-query-editor', template: ' \
      \ + \
    • - my.BackendGDoc = Backbone.Model.extend({ + my.GDoc = Backbone.Model.extend({ sync: function(method, model, options) { var self = this; if (method === "read") { @@ -111,7 +111,7 @@ this.recline.Backend = this.recline.Backend || {}; return results; } }); - recline.Model.backends['gdocs'] = new my.BackendGDoc(); + recline.Model.backends['gdocs'] = new my.GDoc(); }(jQuery, this.recline.Backend)); diff --git a/src/backend/memory.js b/src/backend/memory.js index 4e9fb4fd..4c4e1b63 100644 --- a/src/backend/memory.js +++ b/src/backend/memory.js @@ -1,15 +1,8 @@ -// # Recline Backends -// -// Backends are connectors to backend data sources and stores -// -// Backends are implemented as Backbone models but this is just a -// convenience (they do not save or load themselves from any remote -// source) this.recline = this.recline || {}; this.recline.Backend = this.recline.Backend || {}; (function($, my) { - // ## BackendMemory - uses in-memory data + // ## Memory Backend - uses in-memory data // // This is very artificial and is really only designed for testing // purposes. @@ -23,7 +16,7 @@ this.recline.Backend = this.recline.Backend || {}; // //
         //  // Backend setup
      -  //  var backend = Backend();
      +  //  var backend = recline.Backend.Memory();
         //  backend.addDataset({
         //    metadata: {
         //      id: 'my-id',
      @@ -40,7 +33,7 @@ this.recline.Backend = this.recline.Backend || {};
         //  dataset.fetch();
         //  etc ...
         //  
      - my.BackendMemory = Backbone.Model.extend({ + my.Memory = Backbone.Model.extend({ initialize: function() { this.datasets = {}; }, @@ -82,7 +75,7 @@ this.recline.Backend = this.recline.Backend || {}; } return dfd.promise(); } else { - alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model); + alert('Not supported: sync on Memory backend with method ' + method + ' and model ' + model); } }, query: function(model, queryObj) { @@ -102,6 +95,6 @@ this.recline.Backend = this.recline.Backend || {}; return dfd.promise(); } }); - recline.Model.backends['memory'] = new my.BackendMemory(); + recline.Model.backends['memory'] = new my.Memory(); }(jQuery, this.recline.Backend)); diff --git a/src/backend/webstore.js b/src/backend/webstore.js index d89d99a0..d905d77e 100644 --- a/src/backend/webstore.js +++ b/src/backend/webstore.js @@ -2,12 +2,12 @@ this.recline = this.recline || {}; this.recline.Backend = this.recline.Backend || {}; (function($, my) { - // ## BackendWebstore + // ## Webstore Backend // // Connecting to [Webstores](http://github.com/okfn/webstore) // // To use this backend ensure your Dataset has a webstore_url in its attributes. - my.BackendWebstore = Backbone.Model.extend({ + my.Webstore = Backbone.Model.extend({ sync: function(method, model, options) { if (method === "read") { if (model.__type__ == 'Dataset') { @@ -56,6 +56,6 @@ this.recline.Backend = this.recline.Backend || {}; return dfd.promise(); } }); - recline.Model.backends['webstore'] = new my.BackendWebstore(); + recline.Model.backends['webstore'] = new my.Webstore(); }(jQuery, this.recline.Backend)); diff --git a/test/backend.test.js b/test/backend.test.js index 2ff2db4f..875c584c 100644 --- a/test/backend.test.js +++ b/test/backend.test.js @@ -19,7 +19,7 @@ var memoryData = { }; function makeBackendDataset() { - var backend = new recline.Backend.BackendMemory(); + var backend = new recline.Backend.Memory(); backend.addDataset(memoryData); var dataset = new recline.Model.Dataset({id: memoryData.metadata.id}, backend); return dataset; From a9c1b2ae07182b111e21e8992824b3df67ca196a Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 24 Feb 2012 22:32:30 +0000 Subject: [PATCH 15/24] [#54,backend/es][m]: ElasticSearch backend (readonly atm) - fixes #54. --- src/backend/elasticsearch.js | 75 +++++++++++++++++ test/backend.elasticsearch.test.js | 124 +++++++++++++++++++++++++++++ test/index.html | 2 + 3 files changed, 201 insertions(+) create mode 100644 src/backend/elasticsearch.js create mode 100644 test/backend.elasticsearch.test.js diff --git a/src/backend/elasticsearch.js b/src/backend/elasticsearch.js new file mode 100644 index 00000000..36a7c1a4 --- /dev/null +++ b/src/backend/elasticsearch.js @@ -0,0 +1,75 @@ +this.recline = this.recline || {}; +this.recline.Backend = this.recline.Backend || {}; + +(function($, my) { + // ## ElasticSearch Backend + // + // Connecting to [ElasticSearch](http://www.elasticsearch.org/) + // + // To use this backend ensure your Dataset has a elasticsearch_url, + // webstore_url or url attribute (used in that order) + my.ElasticSearch = Backbone.Model.extend({ + _getESUrl: function(dataset) { + var out = dataset.get('elasticsearch_url'); + if (out) return out; + out = dataset.get('webstore_url'); + if (out) return out; + out = dataset.get('url'); + return out; + }, + sync: function(method, model, options) { + var self = this; + if (method === "read") { + if (model.__type__ == 'Dataset') { + var base = self._getESUrl(model); + var schemaUrl = base + '/_mapping'; + var jqxhr = $.ajax({ + url: schemaUrl, + dataType: 'jsonp' + }); + var dfd = $.Deferred(); + my.wrapInTimeout(jqxhr).done(function(schema) { + // only one top level key in ES = the type so we can ignore it + var key = _.keys(schema)[0]; + var fieldData = _.map(schema[key].properties, function(dict, fieldName) { + dict.id = fieldName; + return dict; + }); + model.fields.reset(fieldData); + dfd.resolve(model, jqxhr); + }) + .fail(function(arguments) { + dfd.reject(arguments); + }); + return dfd.promise(); + } + } else { + alert('This backend currently only supports read operations'); + } + }, + query: function(model, queryObj) { + var base = this._getESUrl(model); + var data = _.extend({}, queryObj); + var jqxhr = $.ajax({ + url: base + '/_search', + data: data, + dataType: 'jsonp' + }); + var dfd = $.Deferred(); + // TODO: fail case + jqxhr.done(function(results) { + model.docCount = results.hits.total; + var docs = _.map(results.hits.hits, function(result) { + var _out = result._source; + _out.id = result._id; + return _out; + }); + dfd.resolve(docs); + }); + return dfd.promise(); + } + }); + recline.Model.backends['elasticsearch'] = new my.ElasticSearch(); + +}(jQuery, this.recline.Backend)); + diff --git a/test/backend.elasticsearch.test.js b/test/backend.elasticsearch.test.js new file mode 100644 index 00000000..d3822c16 --- /dev/null +++ b/test/backend.elasticsearch.test.js @@ -0,0 +1,124 @@ +(function ($) { +module("Backend ElasticSearch"); + +var mapping_data = { + "note": { + "properties": { + "_created": { + "format": "dateOptionalTime", + "type": "date" + }, + "_last_modified": { + "format": "dateOptionalTime", + "type": "date" + }, + "end": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "start": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } +}; + +var sample_data = { + "_shards": { + "failed": 0, + "successful": 5, + "total": 5 + }, + "hits": { + "hits": [ + { + "_id": "u3rpLyuFS3yLNXrtxWkMwg", + "_index": "hypernotes", + "_score": 1.0, + "_source": { + "_created": "2012-02-24T17:53:57.286Z", + "_last_modified": "2012-02-24T17:53:57.286Z", + "owner": "tester", + "title": "Note 1" + }, + "_type": "note" + }, + { + "_id": "n7JMkFOHSASJCVTXgcpqkA", + "_index": "hypernotes", + "_score": 1.0, + "_source": { + "_created": "2012-02-24T17:53:57.290Z", + "_last_modified": "2012-02-24T17:53:57.290Z", + "owner": "tester", + "title": "Note 3" + }, + "_type": "note" + }, + { + "_id": "g7UMA55gTJijvsB3dFitzw", + "_index": "hypernotes", + "_score": 1.0, + "_source": { + "_created": "2012-02-24T17:53:57.289Z", + "_last_modified": "2012-02-24T17:53:57.289Z", + "owner": "tester", + "title": "Note 2" + }, + "_type": "note" + } + ], + "max_score": 1.0, + "total": 3 + }, + "timed_out": false, + "took": 2 +}; + +test("ElasticSearch", function() { + var dataset = new recline.Model.Dataset({ + url: 'https://localhost:9200/my-es-db/my-es-type' + }, + 'elasticsearch' + ); + + var stub = sinon.stub($, 'ajax', function(options) { + if (options.url.indexOf('_mapping') != -1) { + return { + done: function(callback) { + callback(mapping_data); + return this; + }, + fail: function() { + return this; + } + } + } else { + return { + done: function(callback) { + callback(sample_data); + }, + fail: function() { + } + } + } + }); + + dataset.fetch().then(function(dataset) { + deepEqual(['_created', '_last_modified', 'end', 'owner', 'start', 'title'], _.pluck(dataset.fields.toJSON(), 'id')); + dataset.query().then(function(docList) { + equal(3, dataset.docCount); + equal(3, docList.length); + equal('Note 1', docList.models[0].get('title')); + start(); + }); + }); + $.ajax.restore(); +}); + +})(this.jQuery); diff --git a/test/index.html b/test/index.html index 0f16894d..fb96f9cf 100644 --- a/test/index.html +++ b/test/index.html @@ -23,7 +23,9 @@ + + From b4c729b7bb4f0260db34082f5c4b013c0aa96746 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 27 Feb 2012 20:52:25 +0000 Subject: [PATCH 16/24] [#39,demo][s]: add dropdown for selecting type of backend for the url you are importing. --- demo/index.html | 7 +++++++ demo/js/app.js | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/demo/index.html b/demo/index.html index 26a51caf..3b43b444 100644 --- a/demo/index.html +++ b/demo/index.html @@ -29,6 +29,8 @@ + + @@ -45,6 +47,11 @@ + diff --git a/demo/js/app.js b/demo/js/app.js index 54583b94..d3ed89ab 100755 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -76,11 +76,13 @@ function setupLoadFromWebstore(callback) { e.preventDefault(); var $form = $(e.target); var source = $form.find('input[name="source"]').val(); + var type = $form.find('select[name="backend_type"]').val(); var dataset = new recline.Model.Dataset({ - id: 'gold-prices', + id: 'my-dataset', + url: source, webstore_url: source }, - 'webstore' + type ); callback(dataset); }); From bd6123403ea1cfb9eb57a079ded2a122cbc63aea Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 27 Feb 2012 20:54:02 +0000 Subject: [PATCH 17/24] [#34,query][s]: rename offset to from on query object. * More natural in fact and we may as well go whole hog with ES structure. --- src/backend/memory.js | 2 +- src/backend/webstore.js | 2 +- src/model.js | 2 +- src/view.js | 16 ++++++++-------- test/backend.test.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/backend/memory.js b/src/backend/memory.js index 4c4e1b63..688c9194 100644 --- a/src/backend/memory.js +++ b/src/backend/memory.js @@ -80,7 +80,7 @@ this.recline.Backend = this.recline.Backend || {}; }, query: function(model, queryObj) { var numRows = queryObj.size; - var start = queryObj.offset; + var start = queryObj.from; var dfd = $.Deferred(); results = this.datasets[model.id].documents; // not complete sorting! diff --git a/src/backend/webstore.js b/src/backend/webstore.js index d905d77e..b08bfdfa 100644 --- a/src/backend/webstore.js +++ b/src/backend/webstore.js @@ -40,7 +40,7 @@ this.recline.Backend = this.recline.Backend || {}; var base = model.get('webstore_url'); var data = { _limit: queryObj.size - , _offset: queryObj.offset + , _offset: queryObj.from }; var jqxhr = $.ajax({ url: base + '.json', diff --git a/src/model.js b/src/model.js index 853febd2..b4b78594 100644 --- a/src/model.js +++ b/src/model.js @@ -116,7 +116,7 @@ my.FieldList = Backbone.Collection.extend({ my.Query = Backbone.Model.extend({ defaults: { size: 100 - , offset: 0 + , from: 0 } }); diff --git a/src/view.js b/src/view.js index c3139645..4aa7d252 100644 --- a/src/view.js +++ b/src/view.js @@ -186,7 +186,7 @@ my.QueryEditor = Backbone.View.extend({ \ @@ -207,24 +207,24 @@ my.QueryEditor = Backbone.View.extend({ }, onFormSubmit: function(e) { e.preventDefault(); - var newOffset = parseInt(this.el.find('input[name="offset"]').val()); - var newSize = parseInt(this.el.find('input[name="to"]').val()) - newOffset; + var newFrom = parseInt(this.el.find('input[name="from"]').val()); + var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom; var query = this.el.find('.text-query').val(); - this.model.set({size: newSize, offset: newOffset, q: query}); + this.model.set({size: newSize, from: newFrom, q: query}); }, onPaginationUpdate: function(e) { e.preventDefault(); var $el = $(e.target); if ($el.parent().hasClass('prev')) { - var newOffset = this.model.get('offset') - Math.max(0, this.model.get('size')); + var newFrom = this.model.get('from') - Math.max(0, this.model.get('size')); } else { - var newOffset = this.model.get('offset') + this.model.get('size'); + var newFrom = this.model.get('from') + this.model.get('size'); } - this.model.set({offset: newOffset}); + this.model.set({from: newFrom}); }, render: function() { var tmplData = this.model.toJSON(); - tmplData.to = this.model.get('offset') + this.model.get('size'); + tmplData.to = this.model.get('from') + this.model.get('size'); var templated = $.mustache(this.template, tmplData); this.el.html(templated); } diff --git a/test/backend.test.js b/test/backend.test.js index 875c584c..f071085f 100644 --- a/test/backend.test.js +++ b/test/backend.test.js @@ -44,7 +44,7 @@ test('Memory Backend: query', function () { var dataset = makeBackendDataset(); var queryObj = { size: 4 - , offset: 2 + , from: 2 }; dataset.query(queryObj).then(function(documentList) { deepEqual(data.documents[2], documentList.models[0].toJSON()); From ae587506f3e8cddb9cb263145c244400b675d794 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 28 Feb 2012 16:43:37 +0000 Subject: [PATCH 18/24] [#34,query][s]: switch to proper ES style sort structure. --- src/backend/memory.js | 7 ++++--- src/view-grid.js | 8 +++----- test/backend.test.js | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/backend/memory.js b/src/backend/memory.js index 688c9194..d5a43acd 100644 --- a/src/backend/memory.js +++ b/src/backend/memory.js @@ -84,10 +84,11 @@ this.recline.Backend = this.recline.Backend || {}; var dfd = $.Deferred(); results = this.datasets[model.id].documents; // not complete sorting! - _.each(queryObj.sort, function(item) { + _.each(queryObj.sort, function(sortObj) { + var fieldName = _.keys(sortObj)[0]; results = _.sortBy(results, function(doc) { - var _out = doc[item[0]]; - return (item[1] == 'asc') ? _out : -1*_out; + var _out = doc[fieldName]; + return (sortObj[fieldName].order == 'asc') ? _out : -1*_out; }); }); var results = results.slice(start, start+numRows); diff --git a/src/view-grid.js b/src/view-grid.js index feb02b85..3ab4b6fe 100644 --- a/src/view-grid.js +++ b/src/view-grid.js @@ -140,11 +140,9 @@ my.DataGrid = Backbone.View.extend({ }, setColumnSort: function(order) { - this.model.query({ - sort: [ - [this.state.currentColumn, order] - ] - }); + var sort = [{}]; + sort[0][this.state.currentColumn] = {order: order}; + this.model.query({sort: sort}); }, hideColumn: function() { diff --git a/test/backend.test.js b/test/backend.test.js index f071085f..7ba2e913 100644 --- a/test/backend.test.js +++ b/test/backend.test.js @@ -57,7 +57,7 @@ test('Memory Backend: query sort', function () { var data = dataset.backend.datasets[memoryData.metadata.id]; var queryObj = { sort: [ - ['y', 'desc'] + {'y': {order: 'desc'}} ] }; dataset.query(queryObj).then(function(docs) { From 64056da3aac28a9aca6b765916af4483951b894b Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 28 Feb 2012 21:08:18 +0000 Subject: [PATCH 19/24] [backend/elasticsearch][s]: ES backend supports full text queries and sorting. --- src/backend/elasticsearch.js | 26 +++++++++++++++++++++++++- test/backend.elasticsearch.test.js | 21 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/backend/elasticsearch.js b/src/backend/elasticsearch.js index 36a7c1a4..7bcd338e 100644 --- a/src/backend/elasticsearch.js +++ b/src/backend/elasticsearch.js @@ -47,9 +47,33 @@ this.recline.Backend = this.recline.Backend || {}; alert('This backend currently only supports read operations'); } }, + _normalizeQuery: function(queryObj) { + if (queryObj.toJSON) { + var out = queryObj.toJSON(); + } else { + var out = _.extend({}, queryObj); + } + if (out.q != undefined && out.q.trim() === '') { + delete out.q; + } + if (!out.q) { + out.query = { + match_all: {} + } + } else { + out.query = { + query_string: { + query: out.q + } + } + delete out.q; + } + return out; + }, query: function(model, queryObj) { + var queryNormalized = this._normalizeQuery(queryObj); + var data = {source: JSON.stringify(queryNormalized)}; var base = this._getESUrl(model); - var data = _.extend({}, queryObj); var jqxhr = $.ajax({ url: base + '/_search', data: data, diff --git a/test/backend.elasticsearch.test.js b/test/backend.elasticsearch.test.js index d3822c16..4fc931cb 100644 --- a/test/backend.elasticsearch.test.js +++ b/test/backend.elasticsearch.test.js @@ -1,6 +1,27 @@ (function ($) { module("Backend ElasticSearch"); +test("ElasticSearch queryNormalize", function() { + var backend = new recline.Backend.ElasticSearch(); + var in_ = new recline.Model.Query(); + in_.set({q: ''}); + var out = backend._normalizeQuery(in_); + equal(out.q, undefined); + deepEqual(out.query.match_all, {}); + + var backend = new recline.Backend.ElasticSearch(); + var in_ = new recline.Model.Query().toJSON(); + in_.q = ''; + var out = backend._normalizeQuery(in_); + equal(out.q, undefined); + deepEqual(out.query.match_all, {}); + + var in_ = new recline.Model.Query().toJSON(); + in_.q = 'abc'; + var out = backend._normalizeQuery(in_); + equal(out.query.query_string.query, 'abc'); +}); + var mapping_data = { "note": { "properties": { From f770c0dc541b2eeeff82abb3955a72c2261060f0 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 28 Feb 2012 22:24:44 +0000 Subject: [PATCH 20/24] [#52,docs][s]: add info about backends and how to implement one to general docs. --- index.html | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 1aaa14a9..1a580291 100644 --- a/index.html +++ b/index.html @@ -105,13 +105,18 @@

      Documentation

      Recline has a simple structure layered on top of the basic Model/View - distinction inherent in Backbone. There are the following three domain objects (all Backbone Models):

      + distinction inherent in Backbone. There are the following two main domain objects (all Backbone Models):

      • Dataset: represents the dataset. Holds dataset info and a pointer to list of data items (Documents in our terminology) which it can load from the relevant Backend.
      • Document: an individual data item (e.g. a row from a relational database or a spreadsheet, a document from from a document DB like CouchDB or MongoDB).
      • -
      • Backend: provides a way to get data from a specific 'Backend' data source. They provide methods for loading and saving Datasets and individuals Documents as well as for bulk loading via a query API and doing bulk transforms on the backend
      -

      There are then various Views (you can easily write your own). Each view holds a pointer to a Dataset:

      + +

      Backends (more info below) then connect Dataset and Documents to data + from a specific 'Backend' data source. They provide methods for loading and + saving Datasets and individuals Documents as well as for bulk loading via a + query API and doing bulk transforms on the backend.

      + +

      Complementing the model are various Views (you can easily write your own). Each view holds a pointer to a Dataset:

      • DataExplorer: the parent view which manages the overall app and sets up sub views.
      • DataGrid: the data grid view.
      • @@ -143,6 +148,50 @@ Backbone.history.start(); href="demo/">Demo -- just hit view source (NB: the javascript for the demo is in: app.js).

        +

        Backends

        + +

        Backends are connectors to backend data sources from which data can be retrieved.

        + +

        Backends are implemented as Backbone models but this is just a convenience +(they do not save or load themselves from any remote source). You can see +detailed examples of backend implementation in the source documentation +below.

        + +

        A backend must implement two methods:

        +
        +sync(method, model, options)
        +query(dataset, queryObj)
        +
        + +

        sync(method, model, options)

        + +

        This is an implemntation of Backbone.sync and is used to override +Backbone.sync on operations for Datasets and Documents which are using this +backend.

        + +

        For read-only implementations you will need only to implement read method +for Dataset models (and even this can be a null operation). The read method +should return relevant metadata for the Dataset. We do not require read support +for Documents because they are loaded in bulk by the query method.

        + +

        For backends supporting write operations you must implement update and +delete support for Document objects.

        + +

        All code paths should return an object conforming to the jquery promise +API.

        + +

        query(dataset, queryObj)

        + +

        Query the backend for documents returning them in bulk. This method will be +used by the Dataset.query method to search the backend for documents, +retrieving the results in bulk. This method should also set the docCount +attribute on the dataset.

        + +

        queryObj should be either a recline.Model.Query object or a +Hash. The structure of data in the Query object or Hash should follow that +defined in issue 34. (That said, if you are writing your own backend and have +control over the query object you can obviously use whatever structure you +like).

        Source Docs (via Docco)

          From 6fcc09b0283462d8a6daac2a51d1c62bb78e880f Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 28 Feb 2012 22:30:16 +0000 Subject: [PATCH 21/24] [demo][xs]: switch to use recline.js (so we are testing it!) and make ES default in drop-down in topbar. --- demo/index.html | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/demo/index.html b/demo/index.html index 3b43b444..328f01be 100644 --- a/demo/index.html +++ b/demo/index.html @@ -23,18 +23,7 @@ - - - - - - - - - - - - + @@ -48,8 +37,8 @@
          From 6bc48067193c87641c50e102b08b682981abb4f8 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 28 Feb 2012 22:30:50 +0000 Subject: [PATCH 22/24] [build][xs]: add latest built recline.js. --- recline.js | 1381 +++++++++++++++++++++++++++++----------------------- 1 file changed, 785 insertions(+), 596 deletions(-) diff --git a/recline.js b/recline.js index c52f5f53..ce3a2046 100644 --- a/recline.js +++ b/recline.js @@ -1,389 +1,3 @@ -// # Recline Backends -// -// Backends are connectors to backend data sources and stores -// -// Backends are implemented as Backbone models but this is just a -// convenience (they do not save or load themselves from any remote -// source) -this.recline = this.recline || {}; -this.recline.Model = this.recline.Model || {}; - -(function($, my) { - // ## Backbone.sync - // - // Override Backbone.sync to hand off to sync function in relevant backend - Backbone.sync = function(method, model, options) { - return model.backend.sync(method, model, options); - } - - // ## wrapInTimeout - // - // Crude way to catch backend errors - // Many of backends use JSONP and so will not get error messages and this is - // a crude way to catch those errors. - function wrapInTimeout(ourFunction) { - var dfd = $.Deferred(); - var timeout = 5000; - var timer = setTimeout(function() { - dfd.reject({ - message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds' - }); - }, timeout); - ourFunction.done(function(arguments) { - clearTimeout(timer); - dfd.resolve(arguments); - }) - .fail(function(arguments) { - clearTimeout(timer); - dfd.reject(arguments); - }) - ; - return dfd.promise(); - } - - // ## BackendMemory - uses in-memory data - // - // This is very artificial and is really only designed for testing - // purposes. - // - // To use it you should provide in your constructor data: - // - // * metadata (including fields array) - // * documents: list of hashes, each hash being one doc. A doc *must* have an id attribute which is unique. - // - // Example: - // - //
          -  //  // Backend setup
          -  //  var backend = Backend();
          -  //  backend.addDataset({
          -  //    metadata: {
          -  //      id: 'my-id',
          -  //      title: 'My Title'
          -  //    },
          -  //    fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
          -  //    documents: [
          -  //        {id: 0, x: 1, y: 2, z: 3},
          -  //        {id: 1, x: 2, y: 4, z: 6}
          -  //      ]
          -  //  });
          -  //  // later ...
          -  //  var dataset = Dataset({id: 'my-id'});
          -  //  dataset.fetch();
          -  //  etc ...
          -  //  
          - my.BackendMemory = Backbone.Model.extend({ - initialize: function() { - this.datasets = {}; - }, - addDataset: function(data) { - this.datasets[data.metadata.id] = $.extend(true, {}, data); - }, - sync: function(method, model, options) { - var self = this; - if (method === "read") { - var dfd = $.Deferred(); - if (model.__type__ == 'Dataset') { - var rawDataset = this.datasets[model.id]; - model.set(rawDataset.metadata); - model.fields.reset(rawDataset.fields); - model.docCount = rawDataset.documents.length; - dfd.resolve(model); - } - return dfd.promise(); - } else if (method === 'update') { - var dfd = $.Deferred(); - if (model.__type__ == 'Document') { - _.each(self.datasets[model.dataset.id].documents, function(doc, idx) { - if(doc.id === model.id) { - self.datasets[model.dataset.id].documents[idx] = model.toJSON(); - } - }); - dfd.resolve(model); - } - return dfd.promise(); - } else if (method === 'delete') { - var dfd = $.Deferred(); - if (model.__type__ == 'Document') { - var rawDataset = self.datasets[model.dataset.id]; - var newdocs = _.reject(rawDataset.documents, function(doc) { - return (doc.id === model.id); - }); - rawDataset.documents = newdocs; - dfd.resolve(model); - } - return dfd.promise(); - } else { - alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model); - } - }, - query: function(model, queryObj) { - var numRows = queryObj.size; - var start = queryObj.offset; - var dfd = $.Deferred(); - results = this.datasets[model.id].documents; - // not complete sorting! - _.each(queryObj.sort, function(item) { - results = _.sortBy(results, function(doc) { - var _out = doc[item[0]]; - return (item[1] == 'asc') ? _out : -1*_out; - }); - }); - var results = results.slice(start, start+numRows); - dfd.resolve(results); - return dfd.promise(); - } - }); - my.backends['memory'] = new my.BackendMemory(); - - // ## BackendWebstore - // - // Connecting to [Webstores](http://github.com/okfn/webstore) - // - // To use this backend ensure your Dataset has a webstore_url in its attributes. - my.BackendWebstore = Backbone.Model.extend({ - sync: function(method, model, options) { - if (method === "read") { - if (model.__type__ == 'Dataset') { - var base = model.get('webstore_url'); - var schemaUrl = base + '/schema.json'; - var jqxhr = $.ajax({ - url: schemaUrl, - dataType: 'jsonp', - jsonp: '_callback' - }); - var dfd = $.Deferred(); - wrapInTimeout(jqxhr).done(function(schema) { - var fieldData = _.map(schema.data, function(item) { - item.id = item.name; - delete item.name; - return item; - }); - model.fields.reset(fieldData); - model.docCount = schema.count; - dfd.resolve(model, jqxhr); - }) - .fail(function(arguments) { - dfd.reject(arguments); - }); - return dfd.promise(); - } - } - }, - query: function(model, queryObj) { - var base = model.get('webstore_url'); - var data = { - _limit: queryObj.size - , _offset: queryObj.offset - }; - var jqxhr = $.ajax({ - url: base + '.json', - data: data, - dataType: 'jsonp', - jsonp: '_callback', - cache: true - }); - var dfd = $.Deferred(); - jqxhr.done(function(results) { - dfd.resolve(results.data); - }); - return dfd.promise(); - } - }); - my.backends['webstore'] = new my.BackendWebstore(); - - // ## BackendDataProxy - // - // For connecting to [DataProxy-s](http://github.com/okfn/dataproxy). - // - // When initializing the DataProxy backend you can set the following attributes: - // - // * dataproxy: {url-to-proxy} (optional). Defaults to http://jsonpdataproxy.appspot.com - // - // Datasets using using this backend should set the following attributes: - // - // * url: (required) url-of-data-to-proxy - // * format: (optional) csv | xls (defaults to csv if not specified) - // - // Note that this is a **read-only** backend. - my.BackendDataProxy = Backbone.Model.extend({ - defaults: { - dataproxy_url: 'http://jsonpdataproxy.appspot.com' - }, - sync: function(method, model, options) { - var self = this; - if (method === "read") { - if (model.__type__ == 'Dataset') { - var base = self.get('dataproxy_url'); - // TODO: should we cache for extra efficiency - var data = { - url: model.get('url') - , 'max-results': 1 - , type: model.get('format') || 'csv' - }; - var jqxhr = $.ajax({ - url: base - , data: data - , dataType: 'jsonp' - }); - var dfd = $.Deferred(); - wrapInTimeout(jqxhr).done(function(results) { - model.fields.reset(_.map(results.fields, function(fieldId) { - return {id: fieldId}; - }) - ); - dfd.resolve(model, jqxhr); - }) - .fail(function(arguments) { - dfd.reject(arguments); - }); - return dfd.promise(); - } - } else { - alert('This backend only supports read operations'); - } - }, - query: function(dataset, queryObj) { - var base = this.get('dataproxy_url'); - var data = { - url: dataset.get('url') - , 'max-results': queryObj.size - , type: dataset.get('format') - }; - var jqxhr = $.ajax({ - url: base - , data: data - , dataType: 'jsonp' - }); - var dfd = $.Deferred(); - jqxhr.done(function(results) { - var _out = _.map(results.data, function(doc) { - var tmp = {}; - _.each(results.fields, function(key, idx) { - tmp[key] = doc[idx]; - }); - return tmp; - }); - dfd.resolve(_out); - }); - return dfd.promise(); - } - }); - my.backends['dataproxy'] = new my.BackendDataProxy(); - - - // ## Google spreadsheet backend - // - // Connect to Google Docs spreadsheet. - // - // Dataset must have a url attribute pointing to the Gdocs - // spreadsheet's JSON feed e.g. - // - //
          -  // var dataset = new recline.Model.Dataset({
          -  //     url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
          -  //   },
          -  //   'gdocs'
          -  // );
          -  // 
          - my.BackendGDoc = Backbone.Model.extend({ - sync: function(method, model, options) { - var self = this; - if (method === "read") { - var dfd = $.Deferred(); - var dataset = model; - - $.getJSON(model.get('url'), function(d) { - result = self.gdocsToJavascript(d); - model.fields.reset(_.map(result.field, function(fieldId) { - return {id: fieldId}; - }) - ); - // cache data onto dataset (we have loaded whole gdoc it seems!) - model._dataCache = result.data; - dfd.resolve(model); - }) - return dfd.promise(); } - }, - - query: function(dataset, queryObj) { - var dfd = $.Deferred(); - var fields = _.pluck(dataset.fields.toJSON(), 'id'); - - // zip the fields with the data rows to produce js objs - // TODO: factor this out as a common method with other backends - var objs = _.map(dataset._dataCache, function (d) { - var obj = {}; - _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; }) - return obj; - }); - dfd.resolve(objs); - return dfd; - }, - gdocsToJavascript: function(gdocsSpreadsheet) { - /* - :options: (optional) optional argument dictionary: - columnsToUse: list of columns to use (specified by field names) - colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). - :return: tabular data object (hash with keys: field and data). - - Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. - */ - var options = {}; - if (arguments.length > 1) { - options = arguments[1]; - } - var results = { - 'field': [], - 'data': [] - }; - // default is no special info on type of columns - var colTypes = {}; - if (options.colTypes) { - colTypes = options.colTypes; - } - // either extract column headings from spreadsheet directly, or used supplied ones - if (options.columnsToUse) { - // columns set to subset supplied - results.field = options.columnsToUse; - } else { - // set columns to use to be all available - if (gdocsSpreadsheet.feed.entry.length > 0) { - for (var k in gdocsSpreadsheet.feed.entry[0]) { - if (k.substr(0, 3) == 'gsx') { - var col = k.substr(4) - results.field.push(col); - } - } - } - } - - // converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) - var rep = /^([\d\.\-]+)\%$/; - $.each(gdocsSpreadsheet.feed.entry, function (i, entry) { - var row = []; - for (var k in results.field) { - var col = results.field[k]; - var _keyname = 'gsx$' + col; - var value = entry[_keyname]['$t']; - // if labelled as % and value contains %, convert - if (colTypes[col] == 'percent') { - if (rep.test(value)) { - var value2 = rep.exec(value); - var value3 = parseFloat(value2); - value = value3 / 100; - } - } - row.push(value); - } - results.data.push(row); - }); - return results; - } - }); - my.backends['gdocs'] = new my.BackendGDoc(); - -}(jQuery, this.recline.Model)); // importScripts('lib/underscore.js'); onmessage = function(message) { @@ -550,6 +164,7 @@ my.Dataset = Backbone.Model.extend({ // Resulting DocumentList are used to reset this.currentDocuments and are // also returned. query: function(queryObj) { + this.trigger('query:start'); var self = this; this.queryState.set(queryObj, {silent: true}); var dfd = $.Deferred(); @@ -561,9 +176,11 @@ my.Dataset = Backbone.Model.extend({ return _doc; }); self.currentDocuments.reset(docs); + self.trigger('query:done'); dfd.resolve(self.currentDocuments); }) .fail(function(arguments) { + self.trigger('query:fail', arguments); dfd.reject(arguments); }); return dfd.promise(); @@ -624,7 +241,7 @@ my.FieldList = Backbone.Collection.extend({ my.Query = Backbone.Model.extend({ defaults: { size: 100 - , offset: 0 + , from: 0 } }); @@ -800,10 +417,9 @@ var util = function() { this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; -// Views module following classic module pattern (function($, my) { -// Graph view for a Dataset using Flot graphing library. +// ## Graph view for a Dataset using Flot graphing library. // // Initialization arguments: // @@ -937,7 +553,7 @@ my.FlotGraph = Backbone.View.extend({ // Uncaught Invalid dimensions for plot, width = 0, height = 0 // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]); - if (!this.plot && (!areWeVisible || this.model.currentDocuments.length == 0)) { + if ((!areWeVisible || this.model.currentDocuments.length == 0)) { return } // create this.plot and cache it @@ -1038,204 +654,20 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { -// ## DataExplorer -// -// The primary view for the entire application. Usage: -// -//
          -// var myExplorer = new model.recline.DataExplorer({
          -//   model: {{recline.Model.Dataset instance}}
          -//   el: {{an existing dom element}}
          -//   views: {{page views}}
          -//   config: {{config options -- see below}}
          -// });
          -// 
          -// -// ### Parameters -// -// **model**: (required) Dataset instance. -// -// **el**: (required) DOM element. -// -// **views**: (optional) the views (Grid, Graph etc) for DataExplorer to -// show. This is an array of view hashes. If not provided -// just initialize a DataTable with id 'grid'. Example: -// -//
          -// var views = [
          -//   {
          -//     id: 'grid', // used for routing
          -//     label: 'Grid', // used for view switcher
          -//     view: new recline.View.DataTable({
          -//       model: dataset
          -//     })
          -//   },
          -//   {
          -//     id: 'graph',
          -//     label: 'Graph',
          -//     view: new recline.View.FlotGraph({
          -//       model: dataset
          -//     })
          -//   }
          -// ];
          -// 
          -// -// **config**: Config options like: -// -// * displayCount: how many documents to display initially (default: 10) -// * readOnly: true/false (default: false) value indicating whether to -// operate in read-only mode (hiding all editing options). -// -// NB: the element already being in the DOM is important for rendering of -// FlotGraph subview. -my.DataExplorer = Backbone.View.extend({ - template: ' \ -
          \ -
          \ - \ -
          \ - \ - \ -
          \ -
          \ - \ - \ -
          \ - ', - - events: { - 'submit form.display-count': 'onDisplayCountUpdate' - }, - - initialize: function(options) { - var self = this; - this.el = $(this.el); - this.config = _.extend({ - displayCount: 50 - , readOnly: false - }, - options.config); - if (this.config.readOnly) { - this.setReadOnly(); - } - // Hash of 'page' views (i.e. those for whole page) keyed by page name - if (options.views) { - this.pageViews = options.views; - } else { - this.pageViews = [{ - id: 'grid', - label: 'Grid', - view: new my.DataTable({ - model: this.model - }) - }]; - } - // this must be called after pageViews are created - this.render(); - - this.router = new Backbone.Router(); - this.setupRouting(); - - // retrieve basic data like fields etc - // note this.model and dataset returned are the same - this.model.fetch() - .done(function(dataset) { - self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - self.query(); - }) - .fail(function(error) { - my.notify(error.message, {category: 'error', persist: true}); - }); - }, - - query: function() { - this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val()); - var queryObj = { - size: this.config.displayCount - }; - my.notify('Loading data', {loader: true}); - this.model.query(queryObj) - .done(function() { - my.clearNotifications(); - my.notify('Data loaded', {category: 'success'}); - }) - .fail(function(error) { - my.clearNotifications(); - my.notify(error.message, {category: 'error', persist: true}); - }); - }, - - onDisplayCountUpdate: function(e) { - e.preventDefault(); - this.query(); - }, - - setReadOnly: function() { - this.el.addClass('read-only'); - }, - - render: function() { - var tmplData = this.model.toTemplateJSON(); - tmplData.displayCount = this.config.displayCount; - tmplData.views = this.pageViews; - var template = $.mustache(this.template, tmplData); - $(this.el).html(template); - var $dataViewContainer = this.el.find('.data-view-container'); - _.each(this.pageViews, function(view, pageName) { - $dataViewContainer.append(view.view.el) - }); - }, - - setupRouting: function() { - var self = this; - // Default route - this.router.route('', this.pageViews[0].id, function() { - self.updateNav(self.pageViews[0].id); - }); - $.each(this.pageViews, function(idx, view) { - self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) { - self.updateNav(viewId, queryString); - }); - }); - }, - - updateNav: function(pageName, queryString) { - this.el.find('.navigation li').removeClass('active'); - var $el = this.el.find('.navigation li a[href=#' + pageName + ']'); - $el.parent().addClass('active'); - // show the specific page - _.each(this.pageViews, function(view, idx) { - if (view.id === pageName) { - view.view.el.show(); - } else { - view.view.el.hide(); - } - }); - } -}); - -// ## DataTable +// ## DataGrid // // Provides a tabular view on a Dataset. // // Initialize it with a recline.Dataset object. -my.DataTable = Backbone.View.extend({ +// +// Additional options passed in second arguments. Options: +// +// * cellRenderer: function used to render individual cells. See DataGridRow for more. +my.DataGrid = Backbone.View.extend({ tagName: "div", className: "data-table-container", - initialize: function() { + initialize: function(modelEtc, options) { var self = this; this.el = $(this.el); _.bindAll(this, 'render'); @@ -1244,6 +676,7 @@ my.DataTable = Backbone.View.extend({ this.model.currentDocuments.bind('remove', this.render); this.state = {}; this.hiddenFields = []; + this.options = options; }, events: { @@ -1359,11 +792,9 @@ my.DataTable = Backbone.View.extend({ }, setColumnSort: function(order) { - this.model.query({ - sort: [ - [this.state.currentColumn, order] - ] - }); + var sort = [{}]; + sort[0][this.state.currentColumn] = {order: order}; + this.model.query({sort: sort}); }, hideColumn: function() { @@ -1424,11 +855,13 @@ my.DataTable = Backbone.View.extend({ this.model.currentDocuments.forEach(function(doc) { var tr = $(''); self.el.find('tbody').append(tr); - var newView = new my.DataTableRow({ + var newView = new my.DataGridRow({ model: doc, el: tr, fields: self.fields, - }); + }, + self.options + ); newView.render(); }); this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0)); @@ -1436,14 +869,29 @@ my.DataTable = Backbone.View.extend({ } }); -// ## DataTableRow View for rendering an individual document. +// ## DataGridRow View for rendering an individual document. // // Since we want this to update in place it is up to creator to provider the element to attach to. -// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable. -my.DataTableRow = Backbone.View.extend({ - initialize: function(options) { +// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataGrid. +// +// Additional options can be passed in a second hash argument. Options: +// +// * cellRenderer: function to render cells. Signature: function(value, +// field, doc) where value is the value of this cell, field is +// corresponding field object and document is the document object. Note +// that implementing functions can ignore arguments (e.g. +// function(value) would be a valid cellRenderer function). +my.DataGridRow = Backbone.View.extend({ + initialize: function(initData, options) { _.bindAll(this, 'render'); - this._fields = options.fields; + this._fields = initData.fields; + if (options && options.cellRenderer) { + this._cellRenderer = options.cellRenderer; + } else { + this._cellRenderer = function(value) { + return value; + } + } this.el = $(this.el); this.model.bind('change', this.render); }, @@ -1454,22 +902,25 @@ my.DataTableRow = Backbone.View.extend({ \
          \   \ -
          {{value}}
          \ +
          {{{value}}}
          \
          \ \ {{/cells}} \ ', events: { 'click .data-table-cell-edit': 'onEditClick', - // cell editor 'click .data-table-cell-editor .okButton': 'onEditorOK', 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' }, toTemplateJSON: function() { + var self = this; var doc = this.model; var cellData = this._fields.map(function(field) { - return {field: field.id, value: doc.get(field.id)} + return { + field: field.id, + value: self._cellRenderer(doc.get(field.id), field, doc) + } }) return { id: this.id, cells: cellData } }, @@ -1521,6 +972,239 @@ my.DataTableRow = Backbone.View.extend({ } }); +})(jQuery, recline.View); +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { +// ## DataExplorer +// +// The primary view for the entire application. Usage: +// +//
          +// var myExplorer = new model.recline.DataExplorer({
          +//   model: {{recline.Model.Dataset instance}}
          +//   el: {{an existing dom element}}
          +//   views: {{page views}}
          +//   config: {{config options -- see below}}
          +// });
          +// 
          +// +// ### Parameters +// +// **model**: (required) Dataset instance. +// +// **el**: (required) DOM element. +// +// **views**: (optional) the views (Grid, Graph etc) for DataExplorer to +// show. This is an array of view hashes. If not provided +// just initialize a DataGrid with id 'grid'. Example: +// +//
          +// var views = [
          +//   {
          +//     id: 'grid', // used for routing
          +//     label: 'Grid', // used for view switcher
          +//     view: new recline.View.DataGrid({
          +//       model: dataset
          +//     })
          +//   },
          +//   {
          +//     id: 'graph',
          +//     label: 'Graph',
          +//     view: new recline.View.FlotGraph({
          +//       model: dataset
          +//     })
          +//   }
          +// ];
          +// 
          +// +// **config**: Config options like: +// +// * readOnly: true/false (default: false) value indicating whether to +// operate in read-only mode (hiding all editing options). +// +// NB: the element already being in the DOM is important for rendering of +// FlotGraph subview. +my.DataExplorer = Backbone.View.extend({ + template: ' \ +
          \ +
          \ + \ +
          \ + \ +
          \ + Results found {{docCount}} \ +
          \ +
          \ +
          \ + \ + \ +
          \ + ', + + initialize: function(options) { + var self = this; + this.el = $(this.el); + this.config = _.extend({ + readOnly: false + }, + options.config); + if (this.config.readOnly) { + this.setReadOnly(); + } + // Hash of 'page' views (i.e. those for whole page) keyed by page name + if (options.views) { + this.pageViews = options.views; + } else { + this.pageViews = [{ + id: 'grid', + label: 'Grid', + view: new my.DataGrid({ + model: this.model + }) + }]; + } + // this must be called after pageViews are created + this.render(); + + this.router = new Backbone.Router(); + this.setupRouting(); + + this.model.bind('query:start', function(eventName) { + my.notify('Loading data', {loader: true}); + }); + this.model.bind('query:done', function(eventName) { + my.clearNotifications(); + self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); + my.notify('Data loaded', {category: 'success'}); + }); + this.model.bind('query:fail', function(eventName, error) { + my.clearNotifications(); + my.notify(error.message, {category: 'error', persist: true}); + }); + + // retrieve basic data like fields etc + // note this.model and dataset returned are the same + this.model.fetch() + .done(function(dataset) { + self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); + self.model.query(); + }) + .fail(function(error) { + my.notify(error.message, {category: 'error', persist: true}); + }); + }, + + setReadOnly: function() { + this.el.addClass('read-only'); + }, + + render: function() { + var tmplData = this.model.toTemplateJSON(); + tmplData.displayCount = this.config.displayCount; + tmplData.views = this.pageViews; + var template = $.mustache(this.template, tmplData); + $(this.el).html(template); + var $dataViewContainer = this.el.find('.data-view-container'); + _.each(this.pageViews, function(view, pageName) { + $dataViewContainer.append(view.view.el) + }); + var queryEditor = new my.QueryEditor({ + model: this.model.queryState + }); + this.el.find('.header').append(queryEditor.el); + }, + + setupRouting: function() { + var self = this; + // Default route + this.router.route('', this.pageViews[0].id, function() { + self.updateNav(self.pageViews[0].id); + }); + $.each(this.pageViews, function(idx, view) { + self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) { + self.updateNav(viewId, queryString); + }); + }); + }, + + updateNav: function(pageName, queryString) { + this.el.find('.navigation li').removeClass('active'); + var $el = this.el.find('.navigation li a[href=#' + pageName + ']'); + $el.parent().addClass('active'); + // show the specific page + _.each(this.pageViews, function(view, idx) { + if (view.id === pageName) { + view.view.el.show(); + } else { + view.view.el.hide(); + } + }); + } +}); + + +my.QueryEditor = Backbone.View.extend({ + className: 'recline-query-editor', + template: ' \ +
          \ + \ + \ + \ +
          \ + ', + + events: { + 'submit form': 'onFormSubmit', + 'click .action-pagination-update': 'onPaginationUpdate' + }, + + initialize: function() { + _.bindAll(this, 'render'); + this.el = $(this.el); + this.model.bind('change', this.render); + this.render(); + }, + onFormSubmit: function(e) { + e.preventDefault(); + var newFrom = parseInt(this.el.find('input[name="from"]').val()); + var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom; + var query = this.el.find('.text-query').val(); + this.model.set({size: newSize, from: newFrom, q: query}); + }, + onPaginationUpdate: function(e) { + e.preventDefault(); + var $el = $(e.target); + if ($el.parent().hasClass('prev')) { + var newFrom = this.model.get('from') - Math.max(0, this.model.get('size')); + } else { + var newFrom = this.model.get('from') + this.model.get('size'); + } + this.model.set({from: newFrom}); + }, + render: function() { + var tmplData = this.model.toJSON(); + tmplData.to = this.model.get('from') + this.model.get('size'); + var templated = $.mustache(this.template, tmplData); + this.el.html(templated); + } +}); + /* ========================================================== */ // ## Miscellaneous Utilities @@ -1828,3 +1512,508 @@ my.ColumnTransform = Backbone.View.extend({ }); })(jQuery, recline.View); +// # Recline Backends +// +// Backends are connectors to backend data sources and stores +// +// This is just the base module containing various convenience methods. +this.recline = this.recline || {}; +this.recline.Backend = this.recline.Backend || {}; + +(function($, my) { + // ## Backbone.sync + // + // Override Backbone.sync to hand off to sync function in relevant backend + Backbone.sync = function(method, model, options) { + return model.backend.sync(method, model, options); + } + + // ## wrapInTimeout + // + // Crude way to catch backend errors + // Many of backends use JSONP and so will not get error messages and this is + // a crude way to catch those errors. + my.wrapInTimeout = function(ourFunction) { + var dfd = $.Deferred(); + var timeout = 5000; + var timer = setTimeout(function() { + dfd.reject({ + message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds' + }); + }, timeout); + ourFunction.done(function(arguments) { + clearTimeout(timer); + dfd.resolve(arguments); + }) + .fail(function(arguments) { + clearTimeout(timer); + dfd.reject(arguments); + }) + ; + return dfd.promise(); + } +}(jQuery, this.recline.Backend)); + +this.recline = this.recline || {}; +this.recline.Backend = this.recline.Backend || {}; + +(function($, my) { + // ## DataProxy Backend + // + // For connecting to [DataProxy-s](http://github.com/okfn/dataproxy). + // + // When initializing the DataProxy backend you can set the following attributes: + // + // * dataproxy: {url-to-proxy} (optional). Defaults to http://jsonpdataproxy.appspot.com + // + // Datasets using using this backend should set the following attributes: + // + // * url: (required) url-of-data-to-proxy + // * format: (optional) csv | xls (defaults to csv if not specified) + // + // Note that this is a **read-only** backend. + my.DataProxy = Backbone.Model.extend({ + defaults: { + dataproxy_url: 'http://jsonpdataproxy.appspot.com' + }, + sync: function(method, model, options) { + var self = this; + if (method === "read") { + if (model.__type__ == 'Dataset') { + var base = self.get('dataproxy_url'); + // TODO: should we cache for extra efficiency + var data = { + url: model.get('url') + , 'max-results': 1 + , type: model.get('format') || 'csv' + }; + var jqxhr = $.ajax({ + url: base + , data: data + , dataType: 'jsonp' + }); + var dfd = $.Deferred(); + my.wrapInTimeout(jqxhr).done(function(results) { + model.fields.reset(_.map(results.fields, function(fieldId) { + return {id: fieldId}; + }) + ); + dfd.resolve(model, jqxhr); + }) + .fail(function(arguments) { + dfd.reject(arguments); + }); + return dfd.promise(); + } + } else { + alert('This backend only supports read operations'); + } + }, + query: function(dataset, queryObj) { + var base = this.get('dataproxy_url'); + var data = { + url: dataset.get('url') + , 'max-results': queryObj.size + , type: dataset.get('format') + }; + var jqxhr = $.ajax({ + url: base + , data: data + , dataType: 'jsonp' + }); + var dfd = $.Deferred(); + jqxhr.done(function(results) { + var _out = _.map(results.data, function(doc) { + var tmp = {}; + _.each(results.fields, function(key, idx) { + tmp[key] = doc[idx]; + }); + return tmp; + }); + dfd.resolve(_out); + }); + return dfd.promise(); + } + }); + recline.Model.backends['dataproxy'] = new my.DataProxy(); + + +}(jQuery, this.recline.Backend)); +this.recline = this.recline || {}; +this.recline.Backend = this.recline.Backend || {}; + +(function($, my) { + // ## ElasticSearch Backend + // + // Connecting to [ElasticSearch](http://www.elasticsearch.org/) + // + // To use this backend ensure your Dataset has a elasticsearch_url, + // webstore_url or url attribute (used in that order) + my.ElasticSearch = Backbone.Model.extend({ + _getESUrl: function(dataset) { + var out = dataset.get('elasticsearch_url'); + if (out) return out; + out = dataset.get('webstore_url'); + if (out) return out; + out = dataset.get('url'); + return out; + }, + sync: function(method, model, options) { + var self = this; + if (method === "read") { + if (model.__type__ == 'Dataset') { + var base = self._getESUrl(model); + var schemaUrl = base + '/_mapping'; + var jqxhr = $.ajax({ + url: schemaUrl, + dataType: 'jsonp' + }); + var dfd = $.Deferred(); + my.wrapInTimeout(jqxhr).done(function(schema) { + // only one top level key in ES = the type so we can ignore it + var key = _.keys(schema)[0]; + var fieldData = _.map(schema[key].properties, function(dict, fieldName) { + dict.id = fieldName; + return dict; + }); + model.fields.reset(fieldData); + dfd.resolve(model, jqxhr); + }) + .fail(function(arguments) { + dfd.reject(arguments); + }); + return dfd.promise(); + } + } else { + alert('This backend currently only supports read operations'); + } + }, + _normalizeQuery: function(queryObj) { + if (queryObj.toJSON) { + var out = queryObj.toJSON(); + } else { + var out = _.extend({}, queryObj); + } + if (out.q != undefined && out.q.trim() === '') { + delete out.q; + } + if (!out.q) { + out.query = { + match_all: {} + } + } else { + out.query = { + query_string: { + query: out.q + } + } + delete out.q; + } + return out; + }, + query: function(model, queryObj) { + var queryNormalized = this._normalizeQuery(queryObj); + var data = {source: JSON.stringify(queryNormalized)}; + var base = this._getESUrl(model); + var jqxhr = $.ajax({ + url: base + '/_search', + data: data, + dataType: 'jsonp' + }); + var dfd = $.Deferred(); + // TODO: fail case + jqxhr.done(function(results) { + model.docCount = results.hits.total; + var docs = _.map(results.hits.hits, function(result) { + var _out = result._source; + _out.id = result._id; + return _out; + }); + dfd.resolve(docs); + }); + return dfd.promise(); + } + }); + recline.Model.backends['elasticsearch'] = new my.ElasticSearch(); + +}(jQuery, this.recline.Backend)); + +this.recline = this.recline || {}; +this.recline.Backend = this.recline.Backend || {}; + +(function($, my) { + // ## Google spreadsheet backend + // + // Connect to Google Docs spreadsheet. + // + // Dataset must have a url attribute pointing to the Gdocs + // spreadsheet's JSON feed e.g. + // + //
          +  // var dataset = new recline.Model.Dataset({
          +  //     url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
          +  //   },
          +  //   'gdocs'
          +  // );
          +  // 
          + my.GDoc = Backbone.Model.extend({ + sync: function(method, model, options) { + var self = this; + if (method === "read") { + var dfd = $.Deferred(); + var dataset = model; + + $.getJSON(model.get('url'), function(d) { + result = self.gdocsToJavascript(d); + model.fields.reset(_.map(result.field, function(fieldId) { + return {id: fieldId}; + }) + ); + // cache data onto dataset (we have loaded whole gdoc it seems!) + model._dataCache = result.data; + dfd.resolve(model); + }) + return dfd.promise(); } + }, + + query: function(dataset, queryObj) { + var dfd = $.Deferred(); + var fields = _.pluck(dataset.fields.toJSON(), 'id'); + + // zip the fields with the data rows to produce js objs + // TODO: factor this out as a common method with other backends + var objs = _.map(dataset._dataCache, function (d) { + var obj = {}; + _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; }) + return obj; + }); + dfd.resolve(objs); + return dfd; + }, + gdocsToJavascript: function(gdocsSpreadsheet) { + /* + :options: (optional) optional argument dictionary: + columnsToUse: list of columns to use (specified by field names) + colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). + :return: tabular data object (hash with keys: field and data). + + Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. + */ + var options = {}; + if (arguments.length > 1) { + options = arguments[1]; + } + var results = { + 'field': [], + 'data': [] + }; + // default is no special info on type of columns + var colTypes = {}; + if (options.colTypes) { + colTypes = options.colTypes; + } + // either extract column headings from spreadsheet directly, or used supplied ones + if (options.columnsToUse) { + // columns set to subset supplied + results.field = options.columnsToUse; + } else { + // set columns to use to be all available + if (gdocsSpreadsheet.feed.entry.length > 0) { + for (var k in gdocsSpreadsheet.feed.entry[0]) { + if (k.substr(0, 3) == 'gsx') { + var col = k.substr(4) + results.field.push(col); + } + } + } + } + + // converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) + var rep = /^([\d\.\-]+)\%$/; + $.each(gdocsSpreadsheet.feed.entry, function (i, entry) { + var row = []; + for (var k in results.field) { + var col = results.field[k]; + var _keyname = 'gsx$' + col; + var value = entry[_keyname]['$t']; + // if labelled as % and value contains %, convert + if (colTypes[col] == 'percent') { + if (rep.test(value)) { + var value2 = rep.exec(value); + var value3 = parseFloat(value2); + value = value3 / 100; + } + } + row.push(value); + } + results.data.push(row); + }); + return results; + } + }); + recline.Model.backends['gdocs'] = new my.GDoc(); + +}(jQuery, this.recline.Backend)); + +this.recline = this.recline || {}; +this.recline.Backend = this.recline.Backend || {}; + +(function($, my) { + // ## Memory Backend - uses in-memory data + // + // This is very artificial and is really only designed for testing + // purposes. + // + // To use it you should provide in your constructor data: + // + // * metadata (including fields array) + // * documents: list of hashes, each hash being one doc. A doc *must* have an id attribute which is unique. + // + // Example: + // + //
          +  //  // Backend setup
          +  //  var backend = recline.Backend.Memory();
          +  //  backend.addDataset({
          +  //    metadata: {
          +  //      id: 'my-id',
          +  //      title: 'My Title'
          +  //    },
          +  //    fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
          +  //    documents: [
          +  //        {id: 0, x: 1, y: 2, z: 3},
          +  //        {id: 1, x: 2, y: 4, z: 6}
          +  //      ]
          +  //  });
          +  //  // later ...
          +  //  var dataset = Dataset({id: 'my-id'});
          +  //  dataset.fetch();
          +  //  etc ...
          +  //  
          + my.Memory = Backbone.Model.extend({ + initialize: function() { + this.datasets = {}; + }, + addDataset: function(data) { + this.datasets[data.metadata.id] = $.extend(true, {}, data); + }, + sync: function(method, model, options) { + var self = this; + if (method === "read") { + var dfd = $.Deferred(); + if (model.__type__ == 'Dataset') { + var rawDataset = this.datasets[model.id]; + model.set(rawDataset.metadata); + model.fields.reset(rawDataset.fields); + model.docCount = rawDataset.documents.length; + dfd.resolve(model); + } + return dfd.promise(); + } else if (method === 'update') { + var dfd = $.Deferred(); + if (model.__type__ == 'Document') { + _.each(self.datasets[model.dataset.id].documents, function(doc, idx) { + if(doc.id === model.id) { + self.datasets[model.dataset.id].documents[idx] = model.toJSON(); + } + }); + dfd.resolve(model); + } + return dfd.promise(); + } else if (method === 'delete') { + var dfd = $.Deferred(); + if (model.__type__ == 'Document') { + var rawDataset = self.datasets[model.dataset.id]; + var newdocs = _.reject(rawDataset.documents, function(doc) { + return (doc.id === model.id); + }); + rawDataset.documents = newdocs; + dfd.resolve(model); + } + return dfd.promise(); + } else { + alert('Not supported: sync on Memory backend with method ' + method + ' and model ' + model); + } + }, + query: function(model, queryObj) { + var numRows = queryObj.size; + var start = queryObj.from; + var dfd = $.Deferred(); + results = this.datasets[model.id].documents; + // not complete sorting! + _.each(queryObj.sort, function(sortObj) { + var fieldName = _.keys(sortObj)[0]; + results = _.sortBy(results, function(doc) { + var _out = doc[fieldName]; + return (sortObj[fieldName].order == 'asc') ? _out : -1*_out; + }); + }); + var results = results.slice(start, start+numRows); + dfd.resolve(results); + return dfd.promise(); + } + }); + recline.Model.backends['memory'] = new my.Memory(); + +}(jQuery, this.recline.Backend)); +this.recline = this.recline || {}; +this.recline.Backend = this.recline.Backend || {}; + +(function($, my) { + // ## Webstore Backend + // + // Connecting to [Webstores](http://github.com/okfn/webstore) + // + // To use this backend ensure your Dataset has a webstore_url in its attributes. + my.Webstore = Backbone.Model.extend({ + sync: function(method, model, options) { + if (method === "read") { + if (model.__type__ == 'Dataset') { + var base = model.get('webstore_url'); + var schemaUrl = base + '/schema.json'; + var jqxhr = $.ajax({ + url: schemaUrl, + dataType: 'jsonp', + jsonp: '_callback' + }); + var dfd = $.Deferred(); + my.wrapInTimeout(jqxhr).done(function(schema) { + var fieldData = _.map(schema.data, function(item) { + item.id = item.name; + delete item.name; + return item; + }); + model.fields.reset(fieldData); + model.docCount = schema.count; + dfd.resolve(model, jqxhr); + }) + .fail(function(arguments) { + dfd.reject(arguments); + }); + return dfd.promise(); + } + } + }, + query: function(model, queryObj) { + var base = model.get('webstore_url'); + var data = { + _limit: queryObj.size + , _offset: queryObj.from + }; + var jqxhr = $.ajax({ + url: base + '.json', + data: data, + dataType: 'jsonp', + jsonp: '_callback', + cache: true + }); + var dfd = $.Deferred(); + jqxhr.done(function(results) { + dfd.resolve(results.data); + }); + return dfd.promise(); + } + }); + recline.Model.backends['webstore'] = new my.Webstore(); + +}(jQuery, this.recline.Backend)); From 57978c324b1f84019ecc86f9c1fed66bdb1ebbc6 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 28 Feb 2012 22:58:20 +0000 Subject: [PATCH 23/24] [docs,build][m]: build latest version of the docs doing some tidying of the docs along the way. --- docs/backend.html | 353 ------------------------------ docs/backend/base.html | 37 ++++ docs/backend/dataproxy.html | 87 ++++++++ docs/backend/docco.css | 186 ++++++++++++++++ docs/backend/elasticsearch.html | 105 +++++++++ docs/backend/gdocs.html | 98 +++++++++ docs/backend/memory.html | 98 +++++++++ docs/backend/webstore.html | 61 ++++++ docs/model.html | 7 +- docs/view-flot-graph.html | 4 +- docs/view-grid.html | 315 +++++++++++++++++++++++++++ docs/view.html | 367 +++++++------------------------- index.html | 7 +- src/backend/base.js | 4 +- src/backend/elasticsearch.js | 17 +- src/backend/memory.js | 7 +- src/view-grid.js | 21 +- 17 files changed, 1105 insertions(+), 669 deletions(-) delete mode 100644 docs/backend.html create mode 100644 docs/backend/base.html create mode 100644 docs/backend/dataproxy.html create mode 100644 docs/backend/docco.css create mode 100644 docs/backend/elasticsearch.html create mode 100644 docs/backend/gdocs.html create mode 100644 docs/backend/memory.html create mode 100644 docs/backend/webstore.html create mode 100644 docs/view-grid.html diff --git a/docs/backend.html b/docs/backend.html deleted file mode 100644 index b1f5009e..00000000 --- a/docs/backend.html +++ /dev/null @@ -1,353 +0,0 @@ - backend.js

          backend.js

          Recline Backends

          - -

          Backends are connectors to backend data sources and stores

          - -

          Backends are implemented as Backbone models but this is just a -convenience (they do not save or load themselves from any remote -source)

          this.recline = this.recline || {};
          -this.recline.Model = this.recline.Model || {};
          -
          -(function($, my) {

          Backbone.sync

          - -

          Override Backbone.sync to hand off to sync function in relevant backend

            Backbone.sync = function(method, model, options) {
          -    return model.backend.sync(method, model, options);
          -  }

          wrapInTimeout

          - -

          Crude way to catch backend errors -Many of backends use JSONP and so will not get error messages and this is -a crude way to catch those errors.

            function wrapInTimeout(ourFunction) {
          -    var dfd = $.Deferred();
          -    var timeout = 5000;
          -    var timer = setTimeout(function() {
          -      dfd.reject({
          -        message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
          -      });
          -    }, timeout);
          -    ourFunction.done(function(arguments) {
          -        clearTimeout(timer);
          -        dfd.resolve(arguments);
          -      })
          -      .fail(function(arguments) {
          -        clearTimeout(timer);
          -        dfd.reject(arguments);
          -      })
          -      ;
          -    return dfd.promise();
          -  }

          BackendMemory - uses in-memory data

          - -

          This is very artificial and is really only designed for testing -purposes.

          - -

          To use it you should provide in your constructor data:

          - -
            -
          • metadata (including fields array)
          • -
          • documents: list of hashes, each hash being one doc. A doc must have an id attribute which is unique.

            - -

            Example:

            - -
            -// Backend setup
            -var backend = Backend();
            -backend.addDataset({
            -metadata: {
            - id: 'my-id',
            - title: 'My Title'
            -},
            -fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
            -documents: [
            -   {id: 0, x: 1, y: 2, z: 3},
            -   {id: 1, x: 2, y: 4, z: 6}
            - ]
            -});
            -// later ...
            -var dataset = Dataset({id: 'my-id'});
            -dataset.fetch();
            -etc ...
            -
          • -
            my.BackendMemory = Backbone.Model.extend({
          -    initialize: function() {
          -      this.datasets = {};
          -    },
          -    addDataset: function(data) {
          -      this.datasets[data.metadata.id] = $.extend(true, {}, data);
          -    },
          -    sync: function(method, model, options) {
          -      var self = this;
          -      if (method === "read") {
          -        var dfd = $.Deferred();
          -        if (model.__type__ == 'Dataset') {
          -          var rawDataset = this.datasets[model.id];
          -          model.set(rawDataset.metadata);
          -          model.fields.reset(rawDataset.fields);
          -          model.docCount = rawDataset.documents.length;
          -          dfd.resolve(model);
          -        }
          -        return dfd.promise();
          -      } else if (method === 'update') {
          -        var dfd = $.Deferred();
          -        if (model.__type__ == 'Document') {
          -          _.each(self.datasets[model.dataset.id].documents, function(doc, idx) {
          -            if(doc.id === model.id) {
          -              self.datasets[model.dataset.id].documents[idx] = model.toJSON();
          -            }
          -          });
          -          dfd.resolve(model);
          -        }
          -        return dfd.promise();
          -      } else if (method === 'delete') {
          -        var dfd = $.Deferred();
          -        if (model.__type__ == 'Document') {
          -          var rawDataset = self.datasets[model.dataset.id];
          -          var newdocs = _.reject(rawDataset.documents, function(doc) {
          -            return (doc.id === model.id);
          -          });
          -          rawDataset.documents = newdocs;
          -          dfd.resolve(model);
          -        }
          -        return dfd.promise();
          -      } else {
          -        alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model);
          -      }
          -    },
          -    query: function(model, queryObj) {
          -      var numRows = queryObj.size;
          -      var start = queryObj.offset;
          -      var dfd = $.Deferred();
          -      results = this.datasets[model.id].documents;

          not complete sorting!

                _.each(queryObj.sort, function(item) {
          -        results = _.sortBy(results, function(doc) {
          -          var _out = doc[item[0]];
          -          return (item[1] == 'asc') ? _out : -1*_out;
          -        });
          -      });
          -      var results = results.slice(start, start+numRows);
          -      dfd.resolve(results);
          -      return dfd.promise();
          -    }
          -  });
          -  my.backends['memory'] = new my.BackendMemory();

          BackendWebstore

          - -

          Connecting to Webstores

          - -

          To use this backend ensure your Dataset has a webstore_url in its attributes.

            my.BackendWebstore = Backbone.Model.extend({
          -    sync: function(method, model, options) {
          -      if (method === "read") {
          -        if (model.__type__ == 'Dataset') {
          -          var base = model.get('webstore_url');
          -          var schemaUrl = base + '/schema.json';
          -          var jqxhr = $.ajax({
          -            url: schemaUrl,
          -              dataType: 'jsonp',
          -              jsonp: '_callback'
          -          });
          -          var dfd = $.Deferred();
          -          wrapInTimeout(jqxhr).done(function(schema) {
          -            var fieldData = _.map(schema.data, function(item) {
          -              item.id = item.name;
          -              delete item.name;
          -              return item;
          -            });
          -            model.fields.reset(fieldData);
          -            model.docCount = schema.count;
          -            dfd.resolve(model, jqxhr);
          -          })
          -          .fail(function(arguments) {
          -            dfd.reject(arguments);
          -          });
          -          return dfd.promise();
          -        }
          -      }
          -    },
          -    query: function(model, queryObj) {
          -      var base = model.get('webstore_url');
          -      var data = {
          -        _limit:  queryObj.size
          -        , _offset: queryObj.offset
          -      };
          -      var jqxhr = $.ajax({
          -        url: base + '.json',
          -        data: data,
          -        dataType: 'jsonp',
          -        jsonp: '_callback',
          -        cache: true
          -      });
          -      var dfd = $.Deferred();
          -      jqxhr.done(function(results) {
          -        dfd.resolve(results.data);
          -      });
          -      return dfd.promise();
          -    }
          -  });
          -  my.backends['webstore'] = new my.BackendWebstore();

          BackendDataProxy

          - -

          For connecting to DataProxy-s.

          - -

          When initializing the DataProxy backend you can set the following attributes:

          - -
            -
          • dataproxy: {url-to-proxy} (optional). Defaults to http://jsonpdataproxy.appspot.com
          • -
          - -

          Datasets using using this backend should set the following attributes:

          - -
            -
          • url: (required) url-of-data-to-proxy
          • -
          • format: (optional) csv | xls (defaults to csv if not specified)
          • -
          - -

          Note that this is a read-only backend.

            my.BackendDataProxy = Backbone.Model.extend({
          -    defaults: {
          -      dataproxy_url: 'http://jsonpdataproxy.appspot.com'
          -    },
          -    sync: function(method, model, options) {
          -      var self = this;
          -      if (method === "read") {
          -        if (model.__type__ == 'Dataset') {
          -          var base = self.get('dataproxy_url');

          TODO: should we cache for extra efficiency

                    var data = {
          -            url: model.get('url')
          -            , 'max-results':  1
          -            , type: model.get('format') || 'csv'
          -          };
          -          var jqxhr = $.ajax({
          -            url: base
          -            , data: data
          -            , dataType: 'jsonp'
          -          });
          -          var dfd = $.Deferred();
          -          wrapInTimeout(jqxhr).done(function(results) {
          -            model.fields.reset(_.map(results.fields, function(fieldId) {
          -              return {id: fieldId};
          -              })
          -            );
          -            dfd.resolve(model, jqxhr);
          -          })
          -          .fail(function(arguments) {
          -            dfd.reject(arguments);
          -          });
          -          return dfd.promise();
          -        }
          -      } else {
          -        alert('This backend only supports read operations');
          -      }
          -    },
          -    query: function(dataset, queryObj) {
          -      var base = this.get('dataproxy_url');
          -      var data = {
          -        url: dataset.get('url')
          -        , 'max-results':  queryObj.size
          -        , type: dataset.get('format')
          -      };
          -      var jqxhr = $.ajax({
          -        url: base
          -        , data: data
          -        , dataType: 'jsonp'
          -      });
          -      var dfd = $.Deferred();
          -      jqxhr.done(function(results) {
          -        var _out = _.map(results.data, function(doc) {
          -          var tmp = {};
          -          _.each(results.fields, function(key, idx) {
          -            tmp[key] = doc[idx];
          -          });
          -          return tmp;
          -        });
          -        dfd.resolve(_out);
          -      });
          -      return dfd.promise();
          -    }
          -  });
          -  my.backends['dataproxy'] = new my.BackendDataProxy();

          Google spreadsheet backend

          - -

          Connect to Google Docs spreadsheet.

          - -

          Dataset must have a url attribute pointing to the Gdocs -spreadsheet's JSON feed e.g.

          - -
          -var dataset = new recline.Model.Dataset({
          -    url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
          -  },
          -  'gdocs'
          -);
          -
            my.BackendGDoc = Backbone.Model.extend({
          -    sync: function(method, model, options) {
          -      var self = this;
          -      if (method === "read") { 
          -        var dfd = $.Deferred(); 
          -        var dataset = model;
          -
          -        $.getJSON(model.get('url'), function(d) {
          -          result = self.gdocsToJavascript(d);
          -          model.fields.reset(_.map(result.field, function(fieldId) {
          -              return {id: fieldId};
          -            })
          -          );

          cache data onto dataset (we have loaded whole gdoc it seems!)

                    model._dataCache = result.data;
          -          dfd.resolve(model);
          -        })
          -        return dfd.promise(); }
          -    },
          -
          -    query: function(dataset, queryObj) { 
          -      var dfd = $.Deferred();
          -      var fields = _.pluck(dataset.fields.toJSON(), 'id');

          zip the fields with the data rows to produce js objs -TODO: factor this out as a common method with other backends

                var objs = _.map(dataset._dataCache, function (d) { 
          -        var obj = {};
          -        _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
          -        return obj;
          -      });
          -      dfd.resolve(objs);
          -      return dfd;
          -    },
          -    gdocsToJavascript:  function(gdocsSpreadsheet) {
          -      /*
          -         :options: (optional) optional argument dictionary:
          -         columnsToUse: list of columns to use (specified by field names)
          -         colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
          -         :return: tabular data object (hash with keys: field and data).
          -
          -         Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
          -         */
          -      var options = {};
          -      if (arguments.length > 1) {
          -        options = arguments[1];
          -      }
          -      var results = {
          -        'field': [],
          -        'data': []
          -      };

          default is no special info on type of columns

                var colTypes = {};
          -      if (options.colTypes) {
          -        colTypes = options.colTypes;
          -      }

          either extract column headings from spreadsheet directly, or used supplied ones

                if (options.columnsToUse) {

          columns set to subset supplied

                  results.field = options.columnsToUse;
          -      } else {

          set columns to use to be all available

                  if (gdocsSpreadsheet.feed.entry.length > 0) {
          -          for (var k in gdocsSpreadsheet.feed.entry[0]) {
          -            if (k.substr(0, 3) == 'gsx') {
          -              var col = k.substr(4)
          -                results.field.push(col);
          -            }
          -          }
          -        }
          -      }

          converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])

                var rep = /^([\d\.\-]+)\%$/;
          -      $.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
          -        var row = [];
          -        for (var k in results.field) {
          -          var col = results.field[k];
          -          var _keyname = 'gsx$' + col;
          -          var value = entry[_keyname]['$t'];

          if labelled as % and value contains %, convert

                    if (colTypes[col] == 'percent') {
          -            if (rep.test(value)) {
          -              var value2 = rep.exec(value);
          -              var value3 = parseFloat(value2);
          -              value = value3 / 100;
          -            }
          -          }
          -          row.push(value);
          -        }
          -        results.data.push(row);
          -      });
          -      return results;
          -    }
          -  });
          -  my.backends['gdocs'] = new my.BackendGDoc();
          -
          -}(jQuery, this.recline.Model));
          -
          -
          \ No newline at end of file diff --git a/docs/backend/base.html b/docs/backend/base.html new file mode 100644 index 00000000..92c0ca53 --- /dev/null +++ b/docs/backend/base.html @@ -0,0 +1,37 @@ + base.js

          base.js

          Recline Backends

          + +

          Backends are connectors to backend data sources and stores

          + +

          This is just the base module containing various convenience methods.

          this.recline = this.recline || {};
          +this.recline.Backend = this.recline.Backend || {};
          +
          +(function($, my) {

          Backbone.sync

          + +

          Override Backbone.sync to hand off to sync function in relevant backend

            Backbone.sync = function(method, model, options) {
          +    return model.backend.sync(method, model, options);
          +  }

          wrapInTimeout

          + +

          Crude way to catch backend errors +Many of backends use JSONP and so will not get error messages and this is +a crude way to catch those errors.

            my.wrapInTimeout = function(ourFunction) {
          +    var dfd = $.Deferred();
          +    var timeout = 5000;
          +    var timer = setTimeout(function() {
          +      dfd.reject({
          +        message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
          +      });
          +    }, timeout);
          +    ourFunction.done(function(arguments) {
          +        clearTimeout(timer);
          +        dfd.resolve(arguments);
          +      })
          +      .fail(function(arguments) {
          +        clearTimeout(timer);
          +        dfd.reject(arguments);
          +      })
          +      ;
          +    return dfd.promise();
          +  }
          +}(jQuery, this.recline.Backend));
          +
          +
          \ No newline at end of file diff --git a/docs/backend/dataproxy.html b/docs/backend/dataproxy.html new file mode 100644 index 00000000..83405602 --- /dev/null +++ b/docs/backend/dataproxy.html @@ -0,0 +1,87 @@ + dataproxy.js

          dataproxy.js

          this.recline = this.recline || {};
          +this.recline.Backend = this.recline.Backend || {};
          +
          +(function($, my) {

          DataProxy Backend

          + +

          For connecting to DataProxy-s.

          + +

          When initializing the DataProxy backend you can set the following attributes:

          + +
            +
          • dataproxy: {url-to-proxy} (optional). Defaults to http://jsonpdataproxy.appspot.com
          • +
          + +

          Datasets using using this backend should set the following attributes:

          + +
            +
          • url: (required) url-of-data-to-proxy
          • +
          • format: (optional) csv | xls (defaults to csv if not specified)
          • +
          + +

          Note that this is a read-only backend.

            my.DataProxy = Backbone.Model.extend({
          +    defaults: {
          +      dataproxy_url: 'http://jsonpdataproxy.appspot.com'
          +    },
          +    sync: function(method, model, options) {
          +      var self = this;
          +      if (method === "read") {
          +        if (model.__type__ == 'Dataset') {
          +          var base = self.get('dataproxy_url');

          TODO: should we cache for extra efficiency

                    var data = {
          +            url: model.get('url')
          +            , 'max-results':  1
          +            , type: model.get('format') || 'csv'
          +          };
          +          var jqxhr = $.ajax({
          +            url: base
          +            , data: data
          +            , dataType: 'jsonp'
          +          });
          +          var dfd = $.Deferred();
          +          my.wrapInTimeout(jqxhr).done(function(results) {
          +            model.fields.reset(_.map(results.fields, function(fieldId) {
          +              return {id: fieldId};
          +              })
          +            );
          +            dfd.resolve(model, jqxhr);
          +          })
          +          .fail(function(arguments) {
          +            dfd.reject(arguments);
          +          });
          +          return dfd.promise();
          +        }
          +      } else {
          +        alert('This backend only supports read operations');
          +      }
          +    },
          +    query: function(dataset, queryObj) {
          +      var base = this.get('dataproxy_url');
          +      var data = {
          +        url: dataset.get('url')
          +        , 'max-results':  queryObj.size
          +        , type: dataset.get('format')
          +      };
          +      var jqxhr = $.ajax({
          +        url: base
          +        , data: data
          +        , dataType: 'jsonp'
          +      });
          +      var dfd = $.Deferred();
          +      jqxhr.done(function(results) {
          +        var _out = _.map(results.data, function(doc) {
          +          var tmp = {};
          +          _.each(results.fields, function(key, idx) {
          +            tmp[key] = doc[idx];
          +          });
          +          return tmp;
          +        });
          +        dfd.resolve(_out);
          +      });
          +      return dfd.promise();
          +    }
          +  });
          +  recline.Model.backends['dataproxy'] = new my.DataProxy();
          +
          +
          +}(jQuery, this.recline.Backend));
          +
          +
          \ No newline at end of file diff --git a/docs/backend/docco.css b/docs/backend/docco.css new file mode 100644 index 00000000..5aa0a8d7 --- /dev/null +++ b/docs/backend/docco.css @@ -0,0 +1,186 @@ +/*--------------------- Layout and Typography ----------------------------*/ +body { + font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; + font-size: 15px; + line-height: 22px; + color: #252519; + margin: 0; padding: 0; +} +a { + color: #261a3b; +} + a:visited { + color: #261a3b; + } +p { + margin: 0 0 15px 0; +} +h1, h2, h3, h4, h5, h6 { + margin: 0px 0 15px 0; +} + h1 { + margin-top: 40px; + } +#container { + position: relative; +} +#background { + position: fixed; + top: 0; left: 525px; right: 0; bottom: 0; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + z-index: -1; +} +#jump_to, #jump_page { + background: white; + -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; + -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; + font: 10px Arial; + text-transform: uppercase; + cursor: pointer; + text-align: right; +} +#jump_to, #jump_wrapper { + position: fixed; + right: 0; top: 0; + padding: 5px 10px; +} + #jump_wrapper { + padding: 0; + display: none; + } + #jump_to:hover #jump_wrapper { + display: block; + } + #jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; + } + #jump_page .source { + display: block; + padding: 5px 10px; + text-decoration: none; + border-top: 1px solid #eee; + } + #jump_page .source:hover { + background: #f5f5ff; + } + #jump_page .source:first-child { + } +table td { + border: 0; + outline: 0; +} + td.docs, th.docs { + max-width: 450px; + min-width: 450px; + min-height: 5px; + padding: 10px 25px 1px 50px; + overflow-x: hidden; + vertical-align: top; + text-align: left; + } + .docs pre { + margin: 15px 0 15px; + padding-left: 15px; + } + .docs p tt, .docs p code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } + .pilwrap { + position: relative; + } + .pilcrow { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + td.docs:hover .pilcrow { + opacity: 1; + } + td.code, th.code { + padding: 14px 15px 16px 25px; + width: 100%; + vertical-align: top; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + } + pre, tt, code { + font-size: 12px; line-height: 18px; + font-family: Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; + } + + +/*---------------------- Syntax Highlighting -----------------------------*/ +td.linenos { background-color: #f0f0f0; padding-right: 10px; } +span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } +body .hll { background-color: #ffffcc } +body .c { color: #408080; font-style: italic } /* Comment */ +body .err { border: 1px solid #FF0000 } /* Error */ +body .k { color: #954121 } /* Keyword */ +body .o { color: #666666 } /* Operator */ +body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +body .cp { color: #BC7A00 } /* Comment.Preproc */ +body .c1 { color: #408080; font-style: italic } /* Comment.Single */ +body .cs { color: #408080; font-style: italic } /* Comment.Special */ +body .gd { color: #A00000 } /* Generic.Deleted */ +body .ge { font-style: italic } /* Generic.Emph */ +body .gr { color: #FF0000 } /* Generic.Error */ +body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +body .gi { color: #00A000 } /* Generic.Inserted */ +body .go { color: #808080 } /* Generic.Output */ +body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +body .gs { font-weight: bold } /* Generic.Strong */ +body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +body .gt { color: #0040D0 } /* Generic.Traceback */ +body .kc { color: #954121 } /* Keyword.Constant */ +body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ +body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ +body .kp { color: #954121 } /* Keyword.Pseudo */ +body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ +body .kt { color: #B00040 } /* Keyword.Type */ +body .m { color: #666666 } /* Literal.Number */ +body .s { color: #219161 } /* Literal.String */ +body .na { color: #7D9029 } /* Name.Attribute */ +body .nb { color: #954121 } /* Name.Builtin */ +body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +body .no { color: #880000 } /* Name.Constant */ +body .nd { color: #AA22FF } /* Name.Decorator */ +body .ni { color: #999999; font-weight: bold } /* Name.Entity */ +body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +body .nf { color: #0000FF } /* Name.Function */ +body .nl { color: #A0A000 } /* Name.Label */ +body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +body .nt { color: #954121; font-weight: bold } /* Name.Tag */ +body .nv { color: #19469D } /* Name.Variable */ +body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +body .w { color: #bbbbbb } /* Text.Whitespace */ +body .mf { color: #666666 } /* Literal.Number.Float */ +body .mh { color: #666666 } /* Literal.Number.Hex */ +body .mi { color: #666666 } /* Literal.Number.Integer */ +body .mo { color: #666666 } /* Literal.Number.Oct */ +body .sb { color: #219161 } /* Literal.String.Backtick */ +body .sc { color: #219161 } /* Literal.String.Char */ +body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ +body .s2 { color: #219161 } /* Literal.String.Double */ +body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +body .sh { color: #219161 } /* Literal.String.Heredoc */ +body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +body .sx { color: #954121 } /* Literal.String.Other */ +body .sr { color: #BB6688 } /* Literal.String.Regex */ +body .s1 { color: #219161 } /* Literal.String.Single */ +body .ss { color: #19469D } /* Literal.String.Symbol */ +body .bp { color: #954121 } /* Name.Builtin.Pseudo */ +body .vc { color: #19469D } /* Name.Variable.Class */ +body .vg { color: #19469D } /* Name.Variable.Global */ +body .vi { color: #19469D } /* Name.Variable.Instance */ +body .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/backend/elasticsearch.html b/docs/backend/elasticsearch.html new file mode 100644 index 00000000..d576b1cf --- /dev/null +++ b/docs/backend/elasticsearch.html @@ -0,0 +1,105 @@ + elasticsearch.js

          elasticsearch.js

          this.recline = this.recline || {};
          +this.recline.Backend = this.recline.Backend || {};
          +
          +(function($, my) {

          ElasticSearch Backend

          + +

          Connecting to ElasticSearch.

          + +

          To use this backend ensure your Dataset has one of the following +attributes (first one found is used):

          + +
          +elasticsearch_url
          +webstore_url
          +url
          +
          + +

          This should point to the ES type url. E.G. for ES running on +localhost:9200 with index twitter and type tweet it would be

          + +
          http://localhost:9200/twitter/tweet
            my.ElasticSearch = Backbone.Model.extend({
          +    _getESUrl: function(dataset) {
          +      var out = dataset.get('elasticsearch_url');
          +      if (out) return out;
          +      out = dataset.get('webstore_url');
          +      if (out) return out;
          +      out = dataset.get('url');
          +      return out;
          +    },
          +    sync: function(method, model, options) {
          +      var self = this;
          +      if (method === "read") {
          +        if (model.__type__ == 'Dataset') {
          +          var base = self._getESUrl(model);
          +          var schemaUrl = base + '/_mapping';
          +          var jqxhr = $.ajax({
          +            url: schemaUrl,
          +            dataType: 'jsonp'
          +          });
          +          var dfd = $.Deferred();
          +          my.wrapInTimeout(jqxhr).done(function(schema) {

          only one top level key in ES = the type so we can ignore it

                      var key = _.keys(schema)[0];
          +            var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
          +              dict.id = fieldName;
          +              return dict;
          +            });
          +            model.fields.reset(fieldData);
          +            dfd.resolve(model, jqxhr);
          +          })
          +          .fail(function(arguments) {
          +            dfd.reject(arguments);
          +          });
          +          return dfd.promise();
          +        }
          +      } else {
          +        alert('This backend currently only supports read operations');
          +      }
          +    },
          +    _normalizeQuery: function(queryObj) {
          +      if (queryObj.toJSON) {
          +        var out = queryObj.toJSON();
          +      } else {
          +        var out = _.extend({}, queryObj);
          +      }
          +      if (out.q != undefined && out.q.trim() === '') {
          +        delete out.q;
          +      }
          +      if (!out.q) {
          +        out.query = {
          +          match_all: {}
          +        }
          +      } else {
          +        out.query = {
          +          query_string: {
          +            query: out.q
          +          }
          +        }
          +        delete out.q;
          +      }
          +      return out;
          +    },
          +    query: function(model, queryObj) {
          +      var queryNormalized = this._normalizeQuery(queryObj);
          +      var data = {source: JSON.stringify(queryNormalized)};
          +      var base = this._getESUrl(model);
          +      var jqxhr = $.ajax({
          +        url: base + '/_search',
          +        data: data,
          +        dataType: 'jsonp'
          +      });
          +      var dfd = $.Deferred();

          TODO: fail case

                jqxhr.done(function(results) {
          +        model.docCount = results.hits.total;
          +        var docs = _.map(results.hits.hits, function(result) {
          +          var _out = result._source;
          +          _out.id = result._id;
          +          return _out;
          +        });
          +        dfd.resolve(docs);
          +      });
          +      return dfd.promise();
          +    }
          +  });
          +  recline.Model.backends['elasticsearch'] = new my.ElasticSearch();
          +
          +}(jQuery, this.recline.Backend));
          +
          +
          \ No newline at end of file diff --git a/docs/backend/gdocs.html b/docs/backend/gdocs.html new file mode 100644 index 00000000..d503f679 --- /dev/null +++ b/docs/backend/gdocs.html @@ -0,0 +1,98 @@ + gdocs.js

          gdocs.js

          this.recline = this.recline || {};
          +this.recline.Backend = this.recline.Backend || {};
          +
          +(function($, my) {

          Google spreadsheet backend

          + +

          Connect to Google Docs spreadsheet.

          + +

          Dataset must have a url attribute pointing to the Gdocs +spreadsheet's JSON feed e.g.

          + +
          +var dataset = new recline.Model.Dataset({
          +    url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
          +  },
          +  'gdocs'
          +);
          +
            my.GDoc = Backbone.Model.extend({
          +    sync: function(method, model, options) {
          +      var self = this;
          +      if (method === "read") { 
          +        var dfd = $.Deferred(); 
          +        var dataset = model;
          +
          +        $.getJSON(model.get('url'), function(d) {
          +          result = self.gdocsToJavascript(d);
          +          model.fields.reset(_.map(result.field, function(fieldId) {
          +              return {id: fieldId};
          +            })
          +          );

          cache data onto dataset (we have loaded whole gdoc it seems!)

                    model._dataCache = result.data;
          +          dfd.resolve(model);
          +        })
          +        return dfd.promise(); }
          +    },
          +
          +    query: function(dataset, queryObj) { 
          +      var dfd = $.Deferred();
          +      var fields = _.pluck(dataset.fields.toJSON(), 'id');

          zip the fields with the data rows to produce js objs +TODO: factor this out as a common method with other backends

                var objs = _.map(dataset._dataCache, function (d) { 
          +        var obj = {};
          +        _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
          +        return obj;
          +      });
          +      dfd.resolve(objs);
          +      return dfd;
          +    },
          +    gdocsToJavascript:  function(gdocsSpreadsheet) {
          +      /*
          +         :options: (optional) optional argument dictionary:
          +         columnsToUse: list of columns to use (specified by field names)
          +         colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
          +         :return: tabular data object (hash with keys: field and data).
          +
          +         Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
          +         */
          +      var options = {};
          +      if (arguments.length > 1) {
          +        options = arguments[1];
          +      }
          +      var results = {
          +        'field': [],
          +        'data': []
          +      };

          default is no special info on type of columns

                var colTypes = {};
          +      if (options.colTypes) {
          +        colTypes = options.colTypes;
          +      }

          either extract column headings from spreadsheet directly, or used supplied ones

                if (options.columnsToUse) {

          columns set to subset supplied

                  results.field = options.columnsToUse;
          +      } else {

          set columns to use to be all available

                  if (gdocsSpreadsheet.feed.entry.length > 0) {
          +          for (var k in gdocsSpreadsheet.feed.entry[0]) {
          +            if (k.substr(0, 3) == 'gsx') {
          +              var col = k.substr(4)
          +                results.field.push(col);
          +            }
          +          }
          +        }
          +      }

          converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])

                var rep = /^([\d\.\-]+)\%$/;
          +      $.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
          +        var row = [];
          +        for (var k in results.field) {
          +          var col = results.field[k];
          +          var _keyname = 'gsx$' + col;
          +          var value = entry[_keyname]['$t'];

          if labelled as % and value contains %, convert

                    if (colTypes[col] == 'percent') {
          +            if (rep.test(value)) {
          +              var value2 = rep.exec(value);
          +              var value3 = parseFloat(value2);
          +              value = value3 / 100;
          +            }
          +          }
          +          row.push(value);
          +        }
          +        results.data.push(row);
          +      });
          +      return results;
          +    }
          +  });
          +  recline.Model.backends['gdocs'] = new my.GDoc();
          +
          +}(jQuery, this.recline.Backend));
          +
          +
          \ No newline at end of file diff --git a/docs/backend/memory.html b/docs/backend/memory.html new file mode 100644 index 00000000..982be977 --- /dev/null +++ b/docs/backend/memory.html @@ -0,0 +1,98 @@ + memory.js

          memory.js

          this.recline = this.recline || {};
          +this.recline.Backend = this.recline.Backend || {};
          +
          +(function($, my) {

          Memory Backend - uses in-memory data

          + +

          To use it you should provide in your constructor data:

          + +
            +
          • metadata (including fields array)
          • +
          • documents: list of hashes, each hash being one doc. A doc must have an id attribute which is unique.
          • +
          + +

          Example:

          + +

          + // Backend setup
          + var backend = recline.Backend.Memory();
          + backend.addDataset({
          +   metadata: {
          +     id: 'my-id',
          +     title: 'My Title'
          +   },
          +   fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
          +   documents: [
          +       {id: 0, x: 1, y: 2, z: 3},
          +       {id: 1, x: 2, y: 4, z: 6}
          +     ]
          + });
          + // later ...
          + var dataset = Dataset({id: 'my-id'}, 'memory');
          + dataset.fetch();
          + etc ...
          + 

            my.Memory = Backbone.Model.extend({
          +    initialize: function() {
          +      this.datasets = {};
          +    },
          +    addDataset: function(data) {
          +      this.datasets[data.metadata.id] = $.extend(true, {}, data);
          +    },
          +    sync: function(method, model, options) {
          +      var self = this;
          +      if (method === "read") {
          +        var dfd = $.Deferred();
          +        if (model.__type__ == 'Dataset') {
          +          var rawDataset = this.datasets[model.id];
          +          model.set(rawDataset.metadata);
          +          model.fields.reset(rawDataset.fields);
          +          model.docCount = rawDataset.documents.length;
          +          dfd.resolve(model);
          +        }
          +        return dfd.promise();
          +      } else if (method === 'update') {
          +        var dfd = $.Deferred();
          +        if (model.__type__ == 'Document') {
          +          _.each(self.datasets[model.dataset.id].documents, function(doc, idx) {
          +            if(doc.id === model.id) {
          +              self.datasets[model.dataset.id].documents[idx] = model.toJSON();
          +            }
          +          });
          +          dfd.resolve(model);
          +        }
          +        return dfd.promise();
          +      } else if (method === 'delete') {
          +        var dfd = $.Deferred();
          +        if (model.__type__ == 'Document') {
          +          var rawDataset = self.datasets[model.dataset.id];
          +          var newdocs = _.reject(rawDataset.documents, function(doc) {
          +            return (doc.id === model.id);
          +          });
          +          rawDataset.documents = newdocs;
          +          dfd.resolve(model);
          +        }
          +        return dfd.promise();
          +      } else {
          +        alert('Not supported: sync on Memory backend with method ' + method + ' and model ' + model);
          +      }
          +    },
          +    query: function(model, queryObj) {
          +      var numRows = queryObj.size;
          +      var start = queryObj.from;
          +      var dfd = $.Deferred();
          +      results = this.datasets[model.id].documents;

          not complete sorting!

                _.each(queryObj.sort, function(sortObj) {
          +        var fieldName = _.keys(sortObj)[0];
          +        results = _.sortBy(results, function(doc) {
          +          var _out = doc[fieldName];
          +          return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
          +        });
          +      });
          +      var results = results.slice(start, start+numRows);
          +      dfd.resolve(results);
          +      return dfd.promise();
          +    }
          +  });
          +  recline.Model.backends['memory'] = new my.Memory();
          +
          +}(jQuery, this.recline.Backend));
          +
          +
          \ No newline at end of file diff --git a/docs/backend/webstore.html b/docs/backend/webstore.html new file mode 100644 index 00000000..aae1bbc5 --- /dev/null +++ b/docs/backend/webstore.html @@ -0,0 +1,61 @@ + webstore.js

          webstore.js

          this.recline = this.recline || {};
          +this.recline.Backend = this.recline.Backend || {};
          +
          +(function($, my) {

          Webstore Backend

          + +

          Connecting to Webstores

          + +

          To use this backend ensure your Dataset has a webstore_url in its attributes.

            my.Webstore = Backbone.Model.extend({
          +    sync: function(method, model, options) {
          +      if (method === "read") {
          +        if (model.__type__ == 'Dataset') {
          +          var base = model.get('webstore_url');
          +          var schemaUrl = base + '/schema.json';
          +          var jqxhr = $.ajax({
          +            url: schemaUrl,
          +              dataType: 'jsonp',
          +              jsonp: '_callback'
          +          });
          +          var dfd = $.Deferred();
          +          my.wrapInTimeout(jqxhr).done(function(schema) {
          +            var fieldData = _.map(schema.data, function(item) {
          +              item.id = item.name;
          +              delete item.name;
          +              return item;
          +            });
          +            model.fields.reset(fieldData);
          +            model.docCount = schema.count;
          +            dfd.resolve(model, jqxhr);
          +          })
          +          .fail(function(arguments) {
          +            dfd.reject(arguments);
          +          });
          +          return dfd.promise();
          +        }
          +      }
          +    },
          +    query: function(model, queryObj) {
          +      var base = model.get('webstore_url');
          +      var data = {
          +        _limit:  queryObj.size
          +        , _offset: queryObj.from
          +      };
          +      var jqxhr = $.ajax({
          +        url: base + '.json',
          +        data: data,
          +        dataType: 'jsonp',
          +        jsonp: '_callback',
          +        cache: true
          +      });
          +      var dfd = $.Deferred();
          +      jqxhr.done(function(results) {
          +        dfd.resolve(results.data);
          +      });
          +      return dfd.promise();
          +    }
          +  });
          +  recline.Model.backends['webstore'] = new my.Webstore();
          +
          +}(jQuery, this.recline.Backend));
          +
          +
          \ No newline at end of file diff --git a/docs/model.html b/docs/model.html index cabde51b..053d7577 100644 --- a/docs/model.html +++ b/docs/model.html @@ -1,4 +1,4 @@ - model.js

          model.js

          Recline Backbone Models

          this.recline = this.recline || {};
          +      model.js           
          });

          model.js

          Recline Backbone Models

          this.recline = this.recline || {};
           this.recline.Model = this.recline.Model || {};
           
           (function($, my) {

          A Dataset model

          @@ -34,6 +34,7 @@ updated by queryObj (if provided).

          Resulting DocumentList are used to reset this.currentDocuments and are also returned.

            query: function(queryObj) {
          +    this.trigger('query:start');
               var self = this;
               this.queryState.set(queryObj, {silent: true});
               var dfd = $.Deferred();
          @@ -45,9 +46,11 @@ also returned.

          return _doc; }); self.currentDocuments.reset(docs); + self.trigger('query:done'); dfd.resolve(self.currentDocuments); }) .fail(function(arguments) { + self.trigger('query:fail', arguments); dfd.reject(arguments); }); return dfd.promise(); @@ -94,7 +97,7 @@ just pass a single argument representing id to the ctor

          A Query object storing Dataset Query state

          my.Query = Backbone.Model.extend({
             defaults: {
               size: 100
          -    , offset: 0
          +    , from: 0
             }
           });

          Backend registry

          diff --git a/docs/view-flot-graph.html b/docs/view-flot-graph.html index aa18c0b8..dac85e8a 100644 --- a/docs/view-flot-graph.html +++ b/docs/view-flot-graph.html @@ -1,4 +1,4 @@ - view-flot-graph.js

          view-flot-graph.js

          this.recline = this.recline || {};
          +      view-flot-graph.js           

          view-flot-graph.js

          this.recline = this.recline || {};
           this.recline.View = this.recline.View || {};
           
           (function($, my) {

          Graph view for a Dataset using Flot graphing library.

          @@ -126,7 +126,7 @@ TODO: make this less invasive (e.g. preserve other keys in query string)

          Uncaught Invalid dimensions for plot, width = 0, height = 0

        • There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
        •     var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
          -    if (!this.plot && (!areWeVisible || this.model.currentDocuments.length == 0)) {
          +    if ((!areWeVisible || this.model.currentDocuments.length == 0)) {
                 return
               }

          create this.plot and cache it

              if (!this.plot) {

          only lines for the present

                options = {
                   id: 'line',
          diff --git a/docs/view-grid.html b/docs/view-grid.html
          new file mode 100644
          index 00000000..c4c935d5
          --- /dev/null
          +++ b/docs/view-grid.html
          @@ -0,0 +1,315 @@
          +      view-grid.js           

          view-grid.js

          this.recline = this.recline || {};
          +this.recline.View = this.recline.View || {};
          +
          +(function($, my) {

          DataGrid

          + +

          Provides a tabular view on a Dataset.

          + +

          Initialize it with a recline.Dataset object.

          + +

          Additional options passed in second arguments. Options:

          + +
            +
          • cellRenderer: function used to render individual cells. See DataGridRow for more.
          • +
          my.DataGrid = Backbone.View.extend({
          +  tagName:  "div",
          +  className: "data-table-container",
          +
          +  initialize: function(modelEtc, options) {
          +    var self = this;
          +    this.el = $(this.el);
          +    _.bindAll(this, 'render');
          +    this.model.currentDocuments.bind('add', this.render);
          +    this.model.currentDocuments.bind('reset', this.render);
          +    this.model.currentDocuments.bind('remove', this.render);
          +    this.state = {};
          +    this.hiddenFields = [];
          +    this.options = options;
          +  },
          +
          +  events: {
          +    'click .column-header-menu': 'onColumnHeaderClick'
          +    , 'click .row-header-menu': 'onRowHeaderClick'
          +    , 'click .root-header-menu': 'onRootHeaderClick'
          +    , 'click .data-table-menu li a': 'onMenuClick'
          +  },

          TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). +showDialog: function(template, data) { + if (!data) data = {}; + util.show('dialog'); + util.render(template, 'dialog-content', data); + util.observeExit($('.dialog-content'), function() { + util.hide('dialog'); + }) + $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); +},

          ====================================================== +Column and row menus

            onColumnHeaderClick: function(e) {
          +    this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
          +    util.position('data-table-menu', e);
          +    util.render('columnActions', 'data-table-menu');
          +  },
          +
          +  onRowHeaderClick: function(e) {
          +    this.state.currentRow = $(e.target).parents('tr:first').attr('data-id');
          +    util.position('data-table-menu', e);
          +    util.render('rowActions', 'data-table-menu');
          +  },
          +  
          +  onRootHeaderClick: function(e) {
          +    util.position('data-table-menu', e);
          +    util.render('rootActions', 'data-table-menu', {'columns': this.hiddenFields});
          +  },
          +
          +  onMenuClick: function(e) {
          +    var self = this;
          +    e.preventDefault();
          +    var actions = {
          +      bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
          +      transform: function() { self.showTransformDialog('transform') },
          +      sortAsc: function() { self.setColumnSort('asc') },
          +      sortDesc: function() { self.setColumnSort('desc') },
          +      hideColumn: function() { self.hideColumn() },
          +      showColumn: function() { self.showColumn(e) },

          TODO: Delete or re-implement ...

                csv: function() { window.location.href = app.csvUrl },
          +      json: function() { window.location.href = "_rewrite/api/json" },
          +      urlImport: function() { showDialog('urlImport') },
          +      pasteImport: function() { showDialog('pasteImport') },
          +      uploadImport: function() { showDialog('uploadImport') },

          END TODO

                deleteColumn: function() {
          +        var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents.";

          TODO:

                  alert('This function needs to be re-implemented');
          +        return;
          +        if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
          +      },
          +      deleteRow: function() {
          +        var doc = _.find(self.model.currentDocuments.models, function(doc) {

          important this is == as the currentRow will be string (as comes +from DOM) while id may be int

                    return doc.id == self.state.currentRow
          +        });
          +        doc.destroy().then(function() { 
          +            self.model.currentDocuments.remove(doc);
          +            my.notify("Row deleted successfully");
          +          })
          +          .fail(function(err) {
          +            my.notify("Errorz! " + err)
          +          })
          +      }
          +    }
          +    util.hide('data-table-menu');
          +    actions[$(e.target).attr('data-action')]();
          +  },
          +
          +  showTransformColumnDialog: function() {
          +    var $el = $('.dialog-content');
          +    util.show('dialog');
          +    var view = new my.ColumnTransform({
          +      model: this.model
          +    });
          +    view.state = this.state;
          +    view.render();
          +    $el.empty();
          +    $el.append(view.el);
          +    util.observeExit($el, function() {
          +      util.hide('dialog');
          +    })
          +    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
          +  },
          +
          +  showTransformDialog: function() {
          +    var $el = $('.dialog-content');
          +    util.show('dialog');
          +    var view = new recline.View.DataTransform({
          +    });
          +    view.render();
          +    $el.empty();
          +    $el.append(view.el);
          +    util.observeExit($el, function() {
          +      util.hide('dialog');
          +    })
          +    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
          +  },
          +
          +  setColumnSort: function(order) {
          +    var sort = [{}];
          +    sort[0][this.state.currentColumn] = {order: order};
          +    this.model.query({sort: sort});
          +  },
          +  
          +  hideColumn: function() {
          +    this.hiddenFields.push(this.state.currentColumn);
          +    this.render();
          +  },
          +  
          +  showColumn: function(e) {
          +    this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column'));
          +    this.render();
          +  },

          ======================================================

          + +

          Templating

            template: ' \
          +    <div class="data-table-menu-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
          +    <ul class="data-table-menu"></ul> \
          +    <table class="data-table" cellspacing="0"> \
          +      <thead> \
          +        <tr> \
          +          {{#notEmpty}} \
          +            <th class="column-header"> \
          +              <div class="column-header-title"> \
          +                <a class="root-header-menu"></a> \
          +                <span class="column-header-name"></span> \
          +              </div> \
          +            </th> \
          +          {{/notEmpty}} \
          +          {{#fields}} \
          +            <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}"> \
          +              <div class="column-header-title"> \
          +                <a class="column-header-menu"></a> \
          +                <span class="column-header-name">{{label}}</span> \
          +              </div> \
          +              </div> \
          +            </th> \
          +          {{/fields}} \
          +        </tr> \
          +      </thead> \
          +      <tbody></tbody> \
          +    </table> \
          +  ',
          +
          +  toTemplateJSON: function() {
          +    var modelData = this.model.toJSON()
          +    modelData.notEmpty = ( this.fields.length > 0 )

          TODO: move this sort of thing into a toTemplateJSON method on Dataset?

              modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
          +    return modelData;
          +  },
          +  render: function() {
          +    var self = this;
          +    this.fields = this.model.fields.filter(function(field) {
          +      return _.indexOf(self.hiddenFields, field.id) == -1;
          +    });
          +    var htmls = $.mustache(this.template, this.toTemplateJSON());
          +    this.el.html(htmls);
          +    this.model.currentDocuments.forEach(function(doc) {
          +      var tr = $('<tr />');
          +      self.el.find('tbody').append(tr);
          +      var newView = new my.DataGridRow({
          +          model: doc,
          +          el: tr,
          +          fields: self.fields,
          +        },
          +        self.options
          +        );
          +      newView.render();
          +    });
          +    this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
          +    return this;
          +  }
          +});

          DataGridRow View for rendering an individual document.

          + +

          Since we want this to update in place it is up to creator to provider the element to attach to.

          + +

          In addition you must pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.

          + +

          Additional options can be passed in a second hash argument. Options:

          + +
            +
          • cellRenderer: function to render cells. Signature: function(value, +field, doc) where value is the value of this cell, field is +corresponding field object and document is the document object. Note +that implementing functions can ignore arguments (e.g. +function(value) would be a valid cellRenderer function).
          • +
          + +

          Example:

          + +
          +var row = new DataGridRow({
          +  model: dataset-document,
          +    el: dom-element,
          +    fields: mydatasets.fields // a FieldList object
          +  }, {
          +    cellRenderer: my-cell-renderer-function 
          +  }
          +);
          +
          my.DataGridRow = Backbone.View.extend({
          +  initialize: function(initData, options) {
          +    _.bindAll(this, 'render');
          +    this._fields = initData.fields;
          +    if (options && options.cellRenderer) {
          +      this._cellRenderer = options.cellRenderer;
          +    } else {
          +      this._cellRenderer = function(value) {
          +        return value;
          +      }
          +    }
          +    this.el = $(this.el);
          +    this.model.bind('change', this.render);
          +  },
          +
          +  template: ' \
          +      <td><a class="row-header-menu"></a></td> \
          +      {{#cells}} \
          +      <td data-field="{{field}}"> \
          +        <div class="data-table-cell-content"> \
          +          <a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell">&nbsp;</a> \
          +          <div class="data-table-cell-value">{{{value}}}</div> \
          +        </div> \
          +      </td> \
          +      {{/cells}} \
          +    ',
          +  events: {
          +    'click .data-table-cell-edit': 'onEditClick',
          +    'click .data-table-cell-editor .okButton': 'onEditorOK',
          +    'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
          +  },
          +  
          +  toTemplateJSON: function() {
          +    var self = this;
          +    var doc = this.model;
          +    var cellData = this._fields.map(function(field) {
          +      return {
          +        field: field.id,
          +        value: self._cellRenderer(doc.get(field.id), field, doc)
          +      }
          +    })
          +    return { id: this.id, cells: cellData }
          +  },
          +
          +  render: function() {
          +    this.el.attr('data-id', this.model.id);
          +    var html = $.mustache(this.template, this.toTemplateJSON());
          +    $(this.el).html(html);
          +    return this;
          +  },

          =================== +Cell Editor methods

            onEditClick: function(e) {
          +    var editing = this.el.find('.data-table-cell-editor-editor');
          +    if (editing.length > 0) {
          +      editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
          +    }
          +    $(e.target).addClass("hidden");
          +    var cell = $(e.target).siblings('.data-table-cell-value');
          +    cell.data("previousContents", cell.text());
          +    util.render('cellEditor', cell, {value: cell.text()});
          +  },
          +
          +  onEditorOK: function(e) {
          +    var cell = $(e.target);
          +    var rowId = cell.parents('tr').attr('data-id');
          +    var field = cell.parents('td').attr('data-field');
          +    var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
          +    var newData = {};
          +    newData[field] = newValue;
          +    this.model.set(newData);
          +    my.notify("Updating row...", {loader: true});
          +    this.model.save().then(function(response) {
          +        my.notify("Row updated successfully", {category: 'success'});
          +      })
          +      .fail(function() {
          +        my.notify('Error saving row', {
          +          category: 'error',
          +          persist: true
          +        });
          +      });
          +  },
          +
          +  onEditorCancel: function(e) {
          +    var cell = $(e.target).parents('.data-table-cell-value');
          +    cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
          +  }
          +});
          +
          +})(jQuery, recline.View);
          +
          +
          \ No newline at end of file diff --git a/docs/view.html b/docs/view.html index 18edf6c3..3237117f 100644 --- a/docs/view.html +++ b/docs/view.html @@ -1,4 +1,4 @@ - view.js

          view.js

          this.recline = this.recline || {};
          +      view.js           
          _.each(this.pageViews,function(view,pageName){$dataViewContainer.append(view.view.el)}); + varqueryEditor=newmy.QueryEditor({ + model:this.model.queryState + }); + this.el.find('.header').append(queryEditor.el);},setupRouting:function(){ @@ -177,273 +164,63 @@ note this.model and dataset returned are the same

          }});} -});

          view.js

          this.recline = this.recline || {};
           this.recline.View = this.recline.View || {};
           
           (function($, my) {

          DataExplorer

          @@ -22,14 +22,14 @@ var myExplorer = new model.recline.DataExplorer({

          views: (optional) the views (Grid, Graph etc) for DataExplorer to show. This is an array of view hashes. If not provided -just initialize a DataTable with id 'grid'. Example:

          +just initialize a DataGrid with id 'grid'. Example:

           var views = [
             {
               id: 'grid', // used for routing
               label: 'Grid', // used for view switcher
          -    view: new recline.View.DataTable({
          +    view: new recline.View.DataGrid({
                 model: dataset
               })
             },
          @@ -46,7 +46,6 @@ var views = [
           

          config: Config options like:

            -
          • displayCount: how many documents to display initially (default: 10)
          • readOnly: true/false (default: false) value indicating whether to operate in read-only mode (hiding all editing options).
          @@ -63,10 +62,8 @@ FlotGraph subview.

          <li><a href="#{{id}}" class="btn">{{label}}</a> \ {{/views}} \ </ul> \ - <div class="pagination"> \ - <form class="display-count"> \ - Showing 0 to <input name="displayCount" type="text" value="{{displayCount}}" title="Edit and hit enter to change the number of rows displayed" /> of <span class="doc-count">{{docCount}}</span> \ - </form> \ + <div class="recline-results-info"> \ + Results found <span class="doc-count">{{docCount}}</span> \ </div> \ </div> \ <div class="data-view-container"></div> \ @@ -79,16 +76,11 @@ FlotGraph subview.

          </div> \ ', - events: { - 'submit form.display-count': 'onDisplayCountUpdate' - }, - initialize: function(options) { var self = this; this.el = $(this.el); this.config = _.extend({ - displayCount: 50 - , readOnly: false + readOnly: false }, options.config); if (this.config.readOnly) { @@ -99,46 +91,37 @@ FlotGraph subview.

          this.pageViews = [{ id: 'grid', label: 'Grid', - view: new my.DataTable({ + view: new my.DataGrid({ model: this.model }) }]; }

          this must be called after pageViews are created

              this.render();
           
               this.router = new Backbone.Router();
          -    this.setupRouting();

          retrieve basic data like fields etc + this.setupRouting(); + + this.model.bind('query:start', function(eventName) { + my.notify('Loading data', {loader: true}); + }); + this.model.bind('query:done', function(eventName) { + my.clearNotifications(); + self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); + my.notify('Data loaded', {category: 'success'}); + }); + this.model.bind('query:fail', function(eventName, error) { + my.clearNotifications(); + my.notify(error.message, {category: 'error', persist: true}); + });

          retrieve basic data like fields etc note this.model and dataset returned are the same

              this.model.fetch()
                 .done(function(dataset) {
                   self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
          -        self.query();
          +        self.model.query();
                 })
                 .fail(function(error) {
                   my.notify(error.message, {category: 'error', persist: true});
                 });
             },
           
          -  query: function() {
          -    this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
          -    var queryObj = {
          -      size: this.config.displayCount
          -    };
          -    my.notify('Loading data', {loader: true});
          -    this.model.query(queryObj)
          -      .done(function() {
          -        my.clearNotifications();
          -        my.notify('Data loaded', {category: 'success'});
          -      })
          -      .fail(function(error) {
          -        my.clearNotifications();
          -        my.notify(error.message, {category: 'error', persist: true});
          -      });
          -  },
          -
          -  onDisplayCountUpdate: function(e) {
          -    e.preventDefault();
          -    this.query();
          -  },
          -
             setReadOnly: function() {
               this.el.addClass('read-only');
             },
          @@ -153,6 +136,10 @@ note this.model and dataset returned are the same

          DataTable

          +}); -

          Provides a tabular view on a Dataset.

          -

          Initialize it with a recline.Dataset object.

          my.DataTable = Backbone.View.extend({
          -  tagName:  "div",
          -  className: "data-table-container",
          -
          -  initialize: function() {
          -    var self = this;
          -    this.el = $(this.el);
          -    _.bindAll(this, 'render');
          -    this.model.currentDocuments.bind('add', this.render);
          -    this.model.currentDocuments.bind('reset', this.render);
          -    this.model.currentDocuments.bind('remove', this.render);
          -    this.state = {};
          -    this.hiddenFields = [];
          -  },
          -
          -  events: {
          -    'click .column-header-menu': 'onColumnHeaderClick'
          -    , 'click .row-header-menu': 'onRowHeaderClick'
          -    , 'click .root-header-menu': 'onRootHeaderClick'
          -    , 'click .data-table-menu li a': 'onMenuClick'
          -  },

          TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). -showDialog: function(template, data) { - if (!data) data = {}; - util.show('dialog'); - util.render(template, 'dialog-content', data); - util.observeExit($('.dialog-content'), function() { - util.hide('dialog'); - }) - $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); -},

          ====================================================== -Column and row menus

            onColumnHeaderClick: function(e) {
          -    this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
          -    util.position('data-table-menu', e);
          -    util.render('columnActions', 'data-table-menu');
          -  },
          -
          -  onRowHeaderClick: function(e) {
          -    this.state.currentRow = $(e.target).parents('tr:first').attr('data-id');
          -    util.position('data-table-menu', e);
          -    util.render('rowActions', 'data-table-menu');
          -  },
          -  
          -  onRootHeaderClick: function(e) {
          -    util.position('data-table-menu', e);
          -    util.render('rootActions', 'data-table-menu', {'columns': this.hiddenFields});
          -  },
          -
          -  onMenuClick: function(e) {
          -    var self = this;
          -    e.preventDefault();
          -    var actions = {
          -      bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
          -      transform: function() { self.showTransformDialog('transform') },
          -      sortAsc: function() { self.setColumnSort('asc') },
          -      sortDesc: function() { self.setColumnSort('desc') },
          -      hideColumn: function() { self.hideColumn() },
          -      showColumn: function() { self.showColumn(e) },

          TODO: Delete or re-implement ...

                csv: function() { window.location.href = app.csvUrl },
          -      json: function() { window.location.href = "_rewrite/api/json" },
          -      urlImport: function() { showDialog('urlImport') },
          -      pasteImport: function() { showDialog('pasteImport') },
          -      uploadImport: function() { showDialog('uploadImport') },

          END TODO

                deleteColumn: function() {
          -        var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents.";

          TODO:

                  alert('This function needs to be re-implemented');
          -        return;
          -        if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
          -      },
          -      deleteRow: function() {
          -        var doc = _.find(self.model.currentDocuments.models, function(doc) {

          important this is == as the currentRow will be string (as comes -from DOM) while id may be int

                    return doc.id == self.state.currentRow
          -        });
          -        doc.destroy().then(function() { 
          -            self.model.currentDocuments.remove(doc);
          -            my.notify("Row deleted successfully");
          -          })
          -          .fail(function(err) {
          -            my.notify("Errorz! " + err)
          -          })
          -      }
          -    }
          -    util.hide('data-table-menu');
          -    actions[$(e.target).attr('data-action')]();
          -  },
          -
          -  showTransformColumnDialog: function() {
          -    var $el = $('.dialog-content');
          -    util.show('dialog');
          -    var view = new my.ColumnTransform({
          -      model: this.model
          -    });
          -    view.state = this.state;
          -    view.render();
          -    $el.empty();
          -    $el.append(view.el);
          -    util.observeExit($el, function() {
          -      util.hide('dialog');
          -    })
          -    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
          -  },
          -
          -  showTransformDialog: function() {
          -    var $el = $('.dialog-content');
          -    util.show('dialog');
          -    var view = new recline.View.DataTransform({
          -    });
          -    view.render();
          -    $el.empty();
          -    $el.append(view.el);
          -    util.observeExit($el, function() {
          -      util.hide('dialog');
          -    })
          -    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
          -  },
          -
          -  setColumnSort: function(order) {
          -    this.model.query({
          -      sort: [
          -        [this.state.currentColumn, order]
          -      ]
          -    });
          -  },
          -  
          -  hideColumn: function() {
          -    this.hiddenFields.push(this.state.currentColumn);
          -    this.render();
          -  },
          -  
          -  showColumn: function(e) {
          -    this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column'));
          -    this.render();
          -  },

          ======================================================

          - -

          Templating

            template: ' \
          -    <div class="data-table-menu-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
          -    <ul class="data-table-menu"></ul> \
          -    <table class="data-table" cellspacing="0"> \
          -      <thead> \
          -        <tr> \
          -          {{#notEmpty}} \
          -            <th class="column-header"> \
          -              <div class="column-header-title"> \
          -                <a class="root-header-menu"></a> \
          -                <span class="column-header-name"></span> \
          -              </div> \
          -            </th> \
          -          {{/notEmpty}} \
          -          {{#fields}} \
          -            <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}"> \
          -              <div class="column-header-title"> \
          -                <a class="column-header-menu"></a> \
          -                <span class="column-header-name">{{label}}</span> \
          -              </div> \
          -              </div> \
          -            </th> \
          -          {{/fields}} \
          -        </tr> \
          -      </thead> \
          -      <tbody></tbody> \
          -    </table> \
          +my.QueryEditor = Backbone.View.extend({
          +  className: 'recline-query-editor', 
          +  template: ' \
          +    <form action="" method="GET"> \
          +      <input type="text" name="q" value="{{q}}" class="text-query" /> \
          +      <div class="pagination"> \
          +        <ul> \
          +          <li class="prev action-pagination-update"><a>&laquo; back</a></li> \
          +          <li class="active"><a><input name="from" type="text" value="{{from}}" /> &ndash; <input name="to" type="text" value="{{to}}" /> </a></li> \
          +          <li class="next action-pagination-update"><a>next &raquo;</a></li> \
          +        </ul> \
          +      </div> \
          +      <button type="submit" class="btn" style="">Update &raquo;</button> \
          +    </form> \
             ',
           
          -  toTemplateJSON: function() {
          -    var modelData = this.model.toJSON()
          -    modelData.notEmpty = ( this.fields.length > 0 )

          TODO: move this sort of thing into a toTemplateJSON method on Dataset?

              modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
          -    return modelData;
          +  events: {
          +    'submit form': 'onFormSubmit',
          +    'click .action-pagination-update': 'onPaginationUpdate'
             },
          -  render: function() {
          -    var self = this;
          -    this.fields = this.model.fields.filter(function(field) {
          -      return _.indexOf(self.hiddenFields, field.id) == -1;
          -    });
          -    var htmls = $.mustache(this.template, this.toTemplateJSON());
          -    this.el.html(htmls);
          -    this.model.currentDocuments.forEach(function(doc) {
          -      var tr = $('<tr />');
          -      self.el.find('tbody').append(tr);
          -      var newView = new my.DataTableRow({
          -          model: doc,
          -          el: tr,
          -          fields: self.fields,
          -        });
          -      newView.render();
          -    });
          -    this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
          -    return this;
          -  }
          -});

          DataTableRow View for rendering an individual document.

          -

          Since we want this to update in place it is up to creator to provider the element to attach to. -In addition you must pass in a fields in the constructor options. This should be list of fields for the DataTable.

          my.DataTableRow = Backbone.View.extend({
          -  initialize: function(options) {
          +  initialize: function() {
               _.bindAll(this, 'render');
          -    this._fields = options.fields;
               this.el = $(this.el);
               this.model.bind('change', this.render);
          +    this.render();
             },
          -
          -  template: ' \
          -      <td><a class="row-header-menu"></a></td> \
          -      {{#cells}} \
          -      <td data-field="{{field}}"> \
          -        <div class="data-table-cell-content"> \
          -          <a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell">&nbsp;</a> \
          -          <div class="data-table-cell-value">{{value}}</div> \
          -        </div> \
          -      </td> \
          -      {{/cells}} \
          -    ',
          -  events: {
          -    'click .data-table-cell-edit': 'onEditClick',

          cell editor

              'click .data-table-cell-editor .okButton': 'onEditorOK',
          -    'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
          +  onFormSubmit: function(e) {
          +    e.preventDefault();
          +    var newFrom = parseInt(this.el.find('input[name="from"]').val());
          +    var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
          +    var query = this.el.find('.text-query').val();
          +    this.model.set({size: newSize, from: newFrom, q: query});
             },
          -  
          -  toTemplateJSON: function() {
          -    var doc = this.model;
          -    var cellData = this._fields.map(function(field) {
          -      return {field: field.id, value: doc.get(field.id)}
          -    })
          -    return { id: this.id, cells: cellData }
          -  },
          -
          -  render: function() {
          -    this.el.attr('data-id', this.model.id);
          -    var html = $.mustache(this.template, this.toTemplateJSON());
          -    $(this.el).html(html);
          -    return this;
          -  },

          Cell Editor

            onEditClick: function(e) {
          -    var editing = this.el.find('.data-table-cell-editor-editor');
          -    if (editing.length > 0) {
          -      editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
          +  onPaginationUpdate: function(e) {
          +    e.preventDefault();
          +    var $el = $(e.target);
          +    if ($el.parent().hasClass('prev')) {
          +      var newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
          +    } else {
          +      var newFrom = this.model.get('from') + this.model.get('size');
               }
          -    $(e.target).addClass("hidden");
          -    var cell = $(e.target).siblings('.data-table-cell-value');
          -    cell.data("previousContents", cell.text());
          -    util.render('cellEditor', cell, {value: cell.text()});
          +    this.model.set({from: newFrom});
             },
          -
          -  onEditorOK: function(e) {
          -    var cell = $(e.target);
          -    var rowId = cell.parents('tr').attr('data-id');
          -    var field = cell.parents('td').attr('data-field');
          -    var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
          -    var newData = {};
          -    newData[field] = newValue;
          -    this.model.set(newData);
          -    my.notify("Updating row...", {loader: true});
          -    this.model.save().then(function(response) {
          -        my.notify("Row updated successfully", {category: 'success'});
          -      })
          -      .fail(function() {
          -        my.notify('Error saving row', {
          -          category: 'error',
          -          persist: true
          -        });
          -      });
          -  },
          -
          -  onEditorCancel: function(e) {
          -    var cell = $(e.target).parents('.data-table-cell-value');
          -    cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
          +  render: function() {
          +    var tmplData = this.model.toJSON();
          +    tmplData.to = this.model.get('from') + this.model.get('size');
          +    var templated = $.mustache(this.template, tmplData);
          +    this.el.html(templated);
             }
           });
           
           
          -/* ========================================================== */

          Miscellaneous Utilities

          var urlPathRegex = /^([^?]+)(\?.*)?/;

          Parse the Hash section of a URL into path and query string

          my.parseHashUrl = function(hashUrl) {
          +/* ========================================================== */

          Miscellaneous Utilities

          var urlPathRegex = /^([^?]+)(\?.*)?/;

          Parse the Hash section of a URL into path and query string

          my.parseHashUrl = function(hashUrl) {
             var parsed = urlPathRegex.exec(hashUrl);
             if (parsed == null) {
               return {};
          @@ -453,7 +230,7 @@ In addition you must pass in a fields in the constructor options. This should be
                 query: parsed[2] || ''
               }
             }
          -}

          Parse a URL query string (?xyz=abc...) into a dictionary.

          my.parseQueryString = function(q) {
          +}

          Parse a URL query string (?xyz=abc...) into a dictionary.

          my.parseQueryString = function(q) {
             var urlParams = {},
               e, d = function (s) {
                 return unescape(s.replace(/\+/g, " "));
          @@ -463,13 +240,13 @@ In addition you must pass in a fields in the constructor options. This should be
             if (q && q.length && q[0] === '?') {
               q = q.slice(1);
             }
          -  while (e = r.exec(q)) {

          TODO: have values be array as query string allow repetition of keys

              urlParams[d(e[1])] = d(e[2]);
          +  while (e = r.exec(q)) {

          TODO: have values be array as query string allow repetition of keys

              urlParams[d(e[1])] = d(e[2]);
             }
             return urlParams;
          -}

          Parse the query string out of the URL hash

          my.parseHashQueryString = function() {
          +}

          Parse the query string out of the URL hash

          my.parseHashQueryString = function() {
             q = my.parseHashUrl(window.location.hash).query;
             return my.parseQueryString(q);
          -}

          Compse a Query String

          my.composeQueryString = function(queryParams) {
          +}

          Compse a Query String

          my.composeQueryString = function(queryParams) {
             var queryString = '?';
             var items = [];
             $.each(queryParams, function(key, value) {
          @@ -481,7 +258,7 @@ In addition you must pass in a fields in the constructor options. This should be
           
           my.setHashQueryString = function(queryParams) {
             window.location.hash = window.location.hash.split('?')[0] + my.composeQueryString(queryParams);
          -}

          notify

          +}

          notify

          Create a notification (a div.alert-message in div.alert-messsages) using provide messages and options. Options are:

          @@ -513,7 +290,7 @@ In addition you must pass in a fields in the constructor options. This should be }); }, 1000); } -}

          clearNotifications

          +}

          clearNotifications

          Clear all existing notifications

          my.clearNotifications = function() {
             var $notifications = $('.data-explorer .alert-message');
          diff --git a/index.html b/index.html
          index 1a580291..0c18e8ed 100644
          --- a/index.html
          +++ b/index.html
          @@ -196,9 +196,12 @@ like).

          Source Docs (via Docco)

          Tests

          diff --git a/src/backend/base.js b/src/backend/base.js index 60b44225..caf317f9 100644 --- a/src/backend/base.js +++ b/src/backend/base.js @@ -2,9 +2,7 @@ // // Backends are connectors to backend data sources and stores // -// Backends are implemented as Backbone models but this is just a -// convenience (they do not save or load themselves from any remote -// source) +// This is just the base module containing various convenience methods. this.recline = this.recline || {}; this.recline.Backend = this.recline.Backend || {}; diff --git a/src/backend/elasticsearch.js b/src/backend/elasticsearch.js index 7bcd338e..78e27af6 100644 --- a/src/backend/elasticsearch.js +++ b/src/backend/elasticsearch.js @@ -4,10 +4,21 @@ this.recline.Backend = this.recline.Backend || {}; (function($, my) { // ## ElasticSearch Backend // - // Connecting to [ElasticSearch](http://www.elasticsearch.org/) + // Connecting to [ElasticSearch](http://www.elasticsearch.org/). // - // To use this backend ensure your Dataset has a elasticsearch_url, - // webstore_url or url attribute (used in that order) + // To use this backend ensure your Dataset has one of the following + // attributes (first one found is used): + // + //
          +  // elasticsearch_url
          +  // webstore_url
          +  // url
          +  // 
          + // + // This should point to the ES type url. E.G. for ES running on + // localhost:9200 with index twitter and type tweet it would be + // + //
          http://localhost:9200/twitter/tweet
          my.ElasticSearch = Backbone.Model.extend({ _getESUrl: function(dataset) { var out = dataset.get('elasticsearch_url'); diff --git a/src/backend/memory.js b/src/backend/memory.js index d5a43acd..6da45b6b 100644 --- a/src/backend/memory.js +++ b/src/backend/memory.js @@ -4,15 +4,12 @@ this.recline.Backend = this.recline.Backend || {}; (function($, my) { // ## Memory Backend - uses in-memory data // - // This is very artificial and is really only designed for testing - // purposes. - // // To use it you should provide in your constructor data: // // * metadata (including fields array) // * documents: list of hashes, each hash being one doc. A doc *must* have an id attribute which is unique. // - // Example: + // Example: // //
             //  // Backend setup
          @@ -29,7 +26,7 @@ this.recline.Backend = this.recline.Backend || {};
             //      ]
             //  });
             //  // later ...
          -  //  var dataset = Dataset({id: 'my-id'});
          +  //  var dataset = Dataset({id: 'my-id'}, 'memory');
             //  dataset.fetch();
             //  etc ...
             //  
          diff --git a/src/view-grid.js b/src/view-grid.js index 3ab4b6fe..c079226b 100644 --- a/src/view-grid.js +++ b/src/view-grid.js @@ -220,7 +220,8 @@ my.DataGrid = Backbone.View.extend({ // ## DataGridRow View for rendering an individual document. // // Since we want this to update in place it is up to creator to provider the element to attach to. -// In addition you must pass in a fields in the constructor options. This should be list of fields for the DataGrid. +// +// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid. // // Additional options can be passed in a second hash argument. Options: // @@ -229,6 +230,19 @@ my.DataGrid = Backbone.View.extend({ // corresponding field object and document is the document object. Note // that implementing functions can ignore arguments (e.g. // function(value) would be a valid cellRenderer function). +// +// Example: +// +//
          +// var row = new DataGridRow({
          +//   model: dataset-document,
          +//     el: dom-element,
          +//     fields: mydatasets.fields // a FieldList object
          +//   }, {
          +//     cellRenderer: my-cell-renderer-function 
          +//   }
          +// );
          +// 
          my.DataGridRow = Backbone.View.extend({ initialize: function(initData, options) { _.bindAll(this, 'render'); @@ -280,9 +294,8 @@ my.DataGridRow = Backbone.View.extend({ return this; }, - // Cell Editor - // =========== - + // =================== + // Cell Editor methods onEditClick: function(e) { var editing = this.el.find('.data-table-cell-editor-editor'); if (editing.length > 0) { From 0a732b5e06b183648d1491d23dee216f6a8f8d4b Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 28 Feb 2012 22:58:47 +0000 Subject: [PATCH 24/24] [make][s]: crude make file for building docs and recline.js. --- make | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 make diff --git a/make b/make new file mode 100755 index 00000000..9177a082 --- /dev/null +++ b/make @@ -0,0 +1,15 @@ +#!/bin/bash +echo "** Combining js files" +cat src/*.js src/backend/*.js > recline.js + +# build docs +echo "** Building docs" +docco src/model.js src/view.js src/view-grid.js src/view-flot-graph.js +mkdir -p /tmp/recline-docs +mkdir -p docs/backend +PWD=`pwd` +FILES=$PWD/src/backend/*.js +DEST=$PWD/docs/backend +cd /tmp/recline-docs && docco $FILES && mv docs/* $DEST +echo "** Docs built ok" +