zip the fields with the data rows to produce js objs
-TODO: factor this out as a common method with other backends
varobjs=_.map(dataset._dataCache,function(d){
- varobj={};
- _.each(_.zip(fields,d),function(x){obj[x[0]]=x[1];})
- returnobj;
- });
- dfd.resolve(objs);
- returndfd;
- },
- 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.
- */
- varoptions={};
- if(arguments.length>1){
- options=arguments[1];
- }
- varresults={
- 'field':[],
- 'data':[]
- };
\ 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
\ 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
\ 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
zip the fields with the data rows to produce js objs
+TODO: factor this out as a common method with other backends
varobjs=_.map(dataset._dataCache,function(d){
+ varobj={};
+ _.each(_.zip(fields,d),function(x){obj[x[0]]=x[1];})
+ returnobj;
+ });
+ dfd.resolve(objs);
+ returndfd;
+ },
+ 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.
+ */
+ varoptions={};
+ if(arguments.length>1){
+ options=arguments[1];
+ }
+ varresults={
+ 'field':[],
+ 'data':[]
+ };
\ 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
\ 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
\ 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
@@ -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');varself=this;this.queryState.set(queryObj,{silent:true});vardfd=$.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);});returndfd.promise();
@@ -94,7 +97,7 @@ just pass a single argument representing id to the ctor
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).
\ 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
@@ -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.
_.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
}});}
-});
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.
Parse the Hash section of a URL into path and query string
my.parseHashUrl=function(hashUrl){varparsed=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){varurlParams={},e,d=function(s){returnunescape(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)){
my.composeQueryString=function(queryParams){varqueryString='?';varitems=[];$.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);
-}
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
@@ -105,16 +105,21 @@
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.
@@ -143,13 +148,60 @@ 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.
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).
- 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: ' \
-
');
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({
+// 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: ' \
+
+ 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:
+ //
+ //
+ 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));
+
diff --git a/src/backend/memory.js b/src/backend/memory.js
new file mode 100644
index 00000000..6da45b6b
--- /dev/null
+++ b/src/backend/memory.js
@@ -0,0 +1,98 @@
+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:
+ //
+ //
+ 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));
diff --git a/src/backend/webstore.js b/src/backend/webstore.js
new file mode 100644
index 00000000..b08bfdfa
--- /dev/null
+++ b/src/backend/webstore.js
@@ -0,0 +1,61 @@
+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));
diff --git a/src/model.js b/src/model.js
index 932902d9..b4b78594 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();
@@ -113,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-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
diff --git a/src/view-grid.js b/src/view-grid.js
new file mode 100644
index 00000000..c079226b
--- /dev/null
+++ b/src/view-grid.js
@@ -0,0 +1,336 @@
+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: ' \
+
\
+
\
+
\
+ \
+
\
+ {{#notEmpty}} \
+
\
+
\
+ \
+ \
+
\
+
\
+ {{/notEmpty}} \
+ {{#fields}} \
+
\
+
\
+ \
+ {{label}} \
+
\
+ \
+
\
+ {{/fields}} \
+
\
+ \
+ \
+
\
+ ',
+
+ 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.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
+// }
+// );
+//
');
- 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) {
+ initialize: function() {
_.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);
+ this.render();
},
-
- template: ' \
-