From d7e058eb15304f2c4c8edd7bcb0fb8f57ab5b3c4 Mon Sep 17 00:00:00 2001 From: rgrp Date: Sun, 4 Dec 2011 03:36:13 +0000 Subject: [PATCH 01/18] [model,view][s]: (refs #10) refactor to return DocumentList from Dataset.getRows rather than array of simple hashes. --- src/model.js | 15 ++++++++++++++- src/view.js | 20 ++++++++++++-------- test/model.test.js | 18 +++++++++--------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/model.js b/src/model.js index 83f4a49a..d4eac521 100644 --- a/src/model.js +++ b/src/model.js @@ -6,10 +6,23 @@ recline.Dataset = Backbone.Model.extend({ getLength: function() { return this.rowCount; }, + + // Get rows (documents) from the backend returning a recline.DocumentList + // + // TODO: ? rename to getDocuments? + // // this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here. // This also illustrates the limitations of separating the Dataset and the Backend getRows: function(numRows, start) { - return this.backend.getRows(this.id, numRows, start); + var dfd = $.Deferred(); + this.backend.getRows(this.id, numRows, start).then(function(rows) { + var docs = _.map(rows, function(row) { + return new recline.Document(row); + }); + var docList = new recline.DocumentList(docs); + dfd.resolve(docList); + }); + return dfd.promise(); } }); diff --git a/src/view.js b/src/view.js index f580beae..032a4845 100644 --- a/src/view.js +++ b/src/view.js @@ -62,6 +62,9 @@ recline.DataExplorer = Backbone.View.extend({ } }); +// DataTable provides a tabular view on a Dataset. +// +// Initialize it with a recline.Dataset object. recline.DataTable = Backbone.View.extend({ tagName: "div", className: "data-table-container", @@ -69,8 +72,8 @@ recline.DataTable = Backbone.View.extend({ initialize: function() { this.el = $(this.el); var self = this; - this.model.getRows().then(function(rows) { - self._currentRows = rows; + this.model.getRows().then(function(documentList) { + self._currentDocuments = documentList; self.render() }); this.state = {}; @@ -199,9 +202,9 @@ recline.DataTable = Backbone.View.extend({ toTemplateJSON: function() { var modelData = this.model.toJSON() - modelData.rows = _.map(this._currentRows, function(row) { + modelData.rows = _.map(this._currentDocuments.models, function(doc) { var cellData = _.map(modelData.headers, function(header) { - return {header: header, value: row[header]} + return {header: header, value: doc.get(header)} }) return { id: 'xxx', cells: cellData } }) @@ -280,8 +283,8 @@ recline.FlotGraph = Backbone.View.extend({ graphType: 'line' }; var self = this; - this.model.getRows().then(function(rows) { - self._currentRows = rows; + this.model.getRows().then(function(documentList) { + self._currentDocuments = documentList; self.render() }); }, @@ -342,8 +345,9 @@ recline.FlotGraph = Backbone.View.extend({ if (this.chartConfig) { $.each(this.chartConfig.series, function (seriesIndex, field) { var points = []; - $.each(self._currentRows, function (index) { - var x = this[self.chartConfig.group], y = this[field]; + $.each(self._currentDocuments.models, function (index, doc) { + var x = doc.get(self.chartConfig.group); + var y = doc.get(field); if (typeof x === 'string') { x = index; } diff --git a/test/model.test.js b/test/model.test.js index 452349c6..73c01657 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -33,12 +33,12 @@ test('new Dataset', function () { equal(dataset.get('name'), metadata.name); equal(dataset.get('headers'), indata.headers); equal(dataset.getLength(), 6); - dataset.getRows(4, 2).then(function(rows) { - equal(rows[0], indata.rows[2]); + dataset.getRows(4, 2).then(function(documentList) { + deepEqual(indata.rows[2], documentList.models[0].toJSON()); }); - dataset.getRows().then(function(rows) { - equal(rows.length, Math.min(10, indata.rows.length)); - equal(rows[0], indata.rows[0]); + dataset.getRows().then(function(docList) { + equal(docList.length, Math.min(10, indata.rows.length)); + deepEqual(docList.models[0].toJSON(), indata.rows[0]); }); }); }); @@ -136,14 +136,14 @@ test('Webstore Backend', function() { }); dataset.fetch().then(function(dataset) { - equal(['__id__', 'date', 'geometry', 'amount'], dataset.get('headers')); + deepEqual(['__id__', 'date', 'geometry', 'amount'], dataset.get('headers')); equal(3, dataset.rowCount) // restore mocked method $.ajax.restore(); - dataset.getRows().then(function(rows) { + dataset.getRows().then(function(docList) { start(); - equal(3,rows.length) - equal("2009-01-01", rows[0].date); + equal(3, docList.length) + equal("2009-01-01", docList.models[0].get('date')); }); }); }); From d35d3a8986a6c922ae89c56186776016d603039c Mon Sep 17 00:00:00 2001 From: rgrp Date: Wed, 7 Dec 2011 01:53:52 +0000 Subject: [PATCH 02/18] [view,test][m]: (refs #10) introduce DataTablRow view for rendering DataTable row and test it (though have not yet integrated). --- src/view.js | 37 +++++++++++++++++++++++++++++++++++++ test/index.html | 8 +++++--- test/view.test.js | 25 +++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 test/view.test.js diff --git a/src/view.js b/src/view.js index 032a4845..bf1611c9 100644 --- a/src/view.js +++ b/src/view.js @@ -220,6 +220,43 @@ recline.DataTable = Backbone.View.extend({ } }); +// 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 headers in the constructor options. This should be list of headers for the DataTable. +recline.DataTableRow = Backbone.View.extend({ + initialize: function(options) { + this._headers = options.headers; + this.el = $(this.el); + }, + template: ' \ + \ + {{#cells}} \ + \ +
\ +   \ +
{{value}}
\ +
\ + \ + {{/cells}} \ + ', + + toTemplateJSON: function() { + var doc = this.model; + var cellData = _.map(this._headers, function(header) { + return {header: header, value: doc.get(header)} + }) + 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; + } +}); + recline.FlotGraph = Backbone.View.extend({ tagName: "div", diff --git a/test/index.html b/test/index.html index 3c8d8a96..21d80027 100644 --- a/test/index.html +++ b/test/index.html @@ -12,6 +12,8 @@ + +

Qunit Tests

@@ -19,9 +21,9 @@

    -
    -
    -
    +
    + +
    diff --git a/test/view.test.js b/test/view.test.js new file mode 100644 index 00000000..241d748d --- /dev/null +++ b/test/view.test.js @@ -0,0 +1,25 @@ +(function ($) { + +module("View"); + +test('new DataTableRow View', function () { + var $el = $(''); + $('.fixtures .test-datatable').append($el); + var doc = new recline.Document({ + 'id': 1, + 'b': '2', + 'a': '1' + }); + var view = new recline.DataTableRow({ + model: doc + , el: $el + , headers: ['a', 'b'] + }); + view.render(); + ok($el.attr('data-id'), '1'); + var tds = $el.find('td'); + equal(tds.length, 3); + equal($(tds[1]).attr('data-header'), 'a'); +}); + +})(this.jQuery); From 653f59610fb90583ca5df7aa856beaa8d820efed Mon Sep 17 00:00:00 2001 From: rgrp Date: Wed, 7 Dec 2011 13:49:50 +0000 Subject: [PATCH 03/18] [refactor][s]: put model and view objects inside modules of similar name (Model, View). --- demo/js/app.js | 12 ++++++------ src/model.js | 32 +++++++++++++++++++++----------- src/view.js | 22 ++++++++++++++++------ test/model.test.js | 8 ++++---- test/view.test.js | 4 ++-- 5 files changed, 49 insertions(+), 29 deletions(-) diff --git a/demo/js/app.js b/demo/js/app.js index 21f360bc..54eef6fe 100755 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -3,14 +3,14 @@ $(function() { // window.$container = $('.container .right-panel'); window.$container = $('.container'); var dataset = demoDataset(); - window.dataExplorer = new recline.DataExplorer({ + window.dataExplorer = new recline.View.DataExplorer({ model: dataset }); window.$container.append(window.dataExplorer.el); setupLoadFromWebstore(function(dataset) { window.dataExplorer.remove(); window.dataExplorer = null; - window.dataExplorer = new recline.DataExplorer({ + window.dataExplorer = new recline.View.DataExplorer({ model: dataset, }); window.$container.append(window.dataExplorer.el); @@ -36,12 +36,12 @@ function demoDataset() { ] }; // this is all rather artificial here but would make more sense with more complex backend - var backend = new recline.BackendMemory(); + var backend = new recline.Model.BackendMemory(); backend.addDataset({ metadata: metadata, data: indata }); - recline.setBackend(backend); + recline.Model.setBackend(backend); var dataset = backend.getDataset(datasetId); return dataset; } @@ -54,10 +54,10 @@ function setupLoadFromWebstore(callback) { e.preventDefault(); var $form = $(e.target); var source = $form.find('input[name="source"]').val(); - var backend = new recline.BackendWebstore({ + var backend = new recline.Model.BackendWebstore({ url: source }); - recline.setBackend(backend); + recline.Model.setBackend(backend); var dataset = backend.getDataset(); callback(dataset); }); diff --git a/src/model.js b/src/model.js index d4eac521..7a783da7 100644 --- a/src/model.js +++ b/src/model.js @@ -1,7 +1,12 @@ this.recline = this.recline || {}; +// Models module following classic module pattern +recline.Model = function($) { + +var my = {}; + // A Dataset model. -recline.Dataset = Backbone.Model.extend({ +my.Dataset = Backbone.Model.extend({ __type__: 'Dataset', getLength: function() { return this.rowCount; @@ -17,33 +22,33 @@ recline.Dataset = Backbone.Model.extend({ var dfd = $.Deferred(); this.backend.getRows(this.id, numRows, start).then(function(rows) { var docs = _.map(rows, function(row) { - return new recline.Document(row); + return new my.Document(row); }); - var docList = new recline.DocumentList(docs); + var docList = new my.DocumentList(docs); dfd.resolve(docList); }); return dfd.promise(); } }); -recline.Document = Backbone.Model.extend({}); +my.Document = Backbone.Model.extend({}); -recline.DocumentList = Backbone.Collection.extend({ +my.DocumentList = Backbone.Collection.extend({ // webStore: new WebStore(this.url), - model: recline.Document + model: my.Document }) // Backends section // ================ -recline.setBackend = function(backend) { +my.setBackend = function(backend) { Backbone.sync = backend.sync; }; // Backend which just caches in memory // // Does not need to be a backbone model but provides some conveience -recline.BackendMemory = Backbone.Model.extend({ +my.BackendMemory = Backbone.Model.extend({ initialize: function() { this._datasetCache = {} }, @@ -52,7 +57,7 @@ recline.BackendMemory = Backbone.Model.extend({ this._datasetCache[dataset.metadata.id] = dataset; }, getDataset: function(id) { - var dataset = new recline.Dataset({ + var dataset = new my.Dataset({ id: id }); // this is a bit weird but problem is in sync this is set to parent model object so need to give dataset a reference to backend explicitly @@ -97,13 +102,13 @@ recline.BackendMemory = Backbone.Model.extend({ // // Designed to only attached to only dataset and one dataset only ... // Could generalize to support attaching to different datasets -recline.BackendWebstore = Backbone.Model.extend({ +my.BackendWebstore = Backbone.Model.extend({ // require url attribute in initialization data initialize: function() { this.webstoreTableUrl = this.get('url'); }, getDataset: function(id) { - var dataset = new recline.Dataset({ + var dataset = new my.Dataset({ id: id }); dataset.backend = this; @@ -159,3 +164,8 @@ recline.BackendWebstore = Backbone.Model.extend({ return dfd.promise(); } }); + +return my; + +}(jQuery); + diff --git a/src/view.js b/src/view.js index bf1611c9..2658ad82 100644 --- a/src/view.js +++ b/src/view.js @@ -1,6 +1,11 @@ this.recline = this.recline || {}; -recline.DataExplorer = Backbone.View.extend({ +// Views module following classic module pattern +recline.View = function($) { + +var my = {}; + +my.DataExplorer = Backbone.View.extend({ tagName: 'div', className: 'data-explorer', template: ' \ @@ -28,10 +33,10 @@ recline.DataExplorer = Backbone.View.extend({ // note this.model and dataset returned are the same this.model.fetch().then(function(dataset) { // initialize of dataTable calls render - self.dataTable = new recline.DataTable({ + self.dataTable = new my.DataTable({ model: dataset }); - self.flotGraph = new recline.FlotGraph({ + self.flotGraph = new my.FlotGraph({ model: dataset }); self.flotGraph.el.hide(); @@ -65,7 +70,7 @@ recline.DataExplorer = Backbone.View.extend({ // DataTable provides a tabular view on a Dataset. // // Initialize it with a recline.Dataset object. -recline.DataTable = Backbone.View.extend({ +my.DataTable = Backbone.View.extend({ tagName: "div", className: "data-table-container", @@ -224,7 +229,7 @@ recline.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 headers in the constructor options. This should be list of headers for the DataTable. -recline.DataTableRow = Backbone.View.extend({ +my.DataTableRow = Backbone.View.extend({ initialize: function(options) { this._headers = options.headers; this.el = $(this.el); @@ -257,7 +262,7 @@ recline.DataTableRow = Backbone.View.extend({ } }); -recline.FlotGraph = Backbone.View.extend({ +my.FlotGraph = Backbone.View.extend({ tagName: "div", className: "data-graph-container", @@ -411,3 +416,8 @@ recline.FlotGraph = Backbone.View.extend({ return this; } }); + +return my; + +}(jQuery); + diff --git a/test/model.test.js b/test/model.test.js index 73c01657..12e740bd 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -21,12 +21,12 @@ test('new Dataset', function () { ] }; // this is all rather artificial here but would make more sense with more complex backend - backend = new recline.BackendMemory(); + backend = new recline.Model.BackendMemory(); backend.addDataset({ metadata: metadata, data: indata }); - recline.setBackend(backend); + recline.Model.setBackend(backend); var dataset = backend.getDataset(datasetId); expect(6); dataset.fetch().then(function(dataset) { @@ -121,10 +121,10 @@ webstoreData = { test('Webstore Backend', function() { stop(); - var backend = new recline.BackendWebstore({ + var backend = new recline.Model.BackendWebstore({ url: 'http://webstore.test.ckan.org/rufuspollock/demo/data' }); - recline.setBackend(backend); + recline.Model.setBackend(backend); dataset = backend.getDataset(); var stub = sinon.stub($, 'ajax', function(options) { diff --git a/test/view.test.js b/test/view.test.js index 241d748d..e8eb9755 100644 --- a/test/view.test.js +++ b/test/view.test.js @@ -5,12 +5,12 @@ module("View"); test('new DataTableRow View', function () { var $el = $(''); $('.fixtures .test-datatable').append($el); - var doc = new recline.Document({ + var doc = new recline.Model.Document({ 'id': 1, 'b': '2', 'a': '1' }); - var view = new recline.DataTableRow({ + var view = new recline.View.DataTableRow({ model: doc , el: $el , headers: ['a', 'b'] From 39ca647ff3b404ecfc88f5711034517b17f13ad6 Mon Sep 17 00:00:00 2001 From: rgrp Date: Sun, 11 Dec 2011 16:20:02 +0000 Subject: [PATCH 04/18] [reamde][s]: minor refactoring plus add MIT license. --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 00dc3d9f..64841893 100755 --- a/README.md +++ b/README.md @@ -1,26 +1,60 @@ -# Recline - -A pure javascript data explorer and data refinery. Imagine it as a spreadsheet plus Google Refine plus Visualization toolkit, all in pure javascript and html. +Recline DataExplorer is an open-source pure javascript data explorer and data +refinery. Imagine it as a spreadsheet plus Google Refine plus Visualization +toolkit, all in pure javascript and html. Designed for standalone use or as a library to integrate into your own app. + ## Features -* CSV/JSON export your entire database for integration with spreadsheets or - [Google Refine](http://code.google.com/p/google-refine/) +* Open-source (and heavy reuser of existing open-source libraries) +* Pure javascript (no Flash) and designed for integration -- so it is easy to + embed in other sites and applications +* View and edit your data in clean tabular interface * Bulk update/clean your data using an easy scripting UI -* Import by directly downloading from JSON APIs or by uploading files +* Visualize your data ![screenshot](http://i.imgur.com/XDSRe.png) + ## Demo App Open demo/index.html in your favourite browser. -## Minifying dependencies + +## Developer Notes + +### Minifying dependencies npm install -g uglify cd vendor cat *.js | uglifyjs -o ../src/deps-min.js note: make sure underscore.js goes in at the top of the file as a few deps currently depend on it + + +## Copyright and License + +Copyright 2010-2011 Open Knowledge Foundation. + +Licensed under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + From 54c81d68f1987b9b78dc594cdc48f8301c7d73c3 Mon Sep 17 00:00:00 2001 From: rgrp Date: Sun, 11 Dec 2011 16:21:25 +0000 Subject: [PATCH 05/18] [readme][xs]: correct incorrect copyright message. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64841893..aeb750ff 100755 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ note: make sure underscore.js goes in at the top of the file as a few deps curre ## Copyright and License -Copyright 2010-2011 Open Knowledge Foundation. +Copyright 2011 Max Ogden and Rufus Pollock. Licensed under the MIT license: From 01fa7343ef72b48075afd2e5e28a4644680fae5d Mon Sep 17 00:00:00 2001 From: rgrp Date: Tue, 13 Dec 2011 05:50:45 +0000 Subject: [PATCH 06/18] [refactor][xs]: move DataTable template in view from demo/index.html. --- src/view.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/view.js b/src/view.js index 2658ad82..b477b038 100644 --- a/src/view.js +++ b/src/view.js @@ -204,6 +204,37 @@ my.DataTable = Backbone.View.extend({ // ====================================================== // Core Templating + template: ' \ + \ + \ + \ + {{#notEmpty}}{{/notEmpty}} \ + {{#headers}} \ + \ + {{/headers}} \ + \ + {{#rows}} \ + \ + \ + {{#cells}} \ + \ + {{/cells}} \ + \ + {{/rows}} \ + \ +
    \ +
    \ + \ + {{.}} \ +
    \ + \ +
    \ +
    \ +   \ +
    {{value}}
    \ +
    \ +
    \ + ', toTemplateJSON: function() { var modelData = this.model.toJSON() From e741ee7fff92989699e520584b09ada5183e3a8c Mon Sep 17 00:00:00 2001 From: rgrp Date: Tue, 13 Dec 2011 06:13:13 +0000 Subject: [PATCH 07/18] [view][s]: (refs #14) switched DataTable view to render using DataTableRow view (rendering now in pure js!). --- src/view.js | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/view.js b/src/view.js index b477b038..e1913d6f 100644 --- a/src/view.js +++ b/src/view.js @@ -206,7 +206,7 @@ my.DataTable = Backbone.View.extend({ // Core Templating template: ' \ \ - \ + \ \ {{#notEmpty}}{{/notEmpty}} \ {{#headers}} \ @@ -219,39 +219,32 @@ my.DataTable = Backbone.View.extend({ \ {{/headers}} \ \ - {{#rows}} \ - \ - \ - {{#cells}} \ - \ - {{/cells}} \ - \ - {{/rows}} \ - \ + \ + \
    \ -
    \ -   \ -
    {{value}}
    \ -
    \ -
    \ ', toTemplateJSON: function() { var modelData = this.model.toJSON() - modelData.rows = _.map(this._currentDocuments.models, function(doc) { - var cellData = _.map(modelData.headers, function(header) { - return {header: header, value: doc.get(header)} - }) - return { id: 'xxx', cells: cellData } - }) modelData.notEmpty = ( modelData.headers.length > 0 ) return modelData; }, render: function() { + var self = this; var template = $( ".dataTableTemplate:first" ).html() , htmls = $.mustache(template, this.toTemplateJSON()) ; - $(this.el).html(htmls); + this.el.html(htmls); + this._currentDocuments.forEach(function(doc) { + var tr = $(''); + self.el.find('tbody').append(tr); + var newView = new my.DataTableRow({ + model: doc, + el: tr, + headers: self.model.get('headers') + }); + newView.render(); + }); return this; } }); @@ -264,6 +257,7 @@ my.DataTableRow = Backbone.View.extend({ initialize: function(options) { this._headers = options.headers; this.el = $(this.el); + this.model.bind('change', this.render); }, template: ' \ \ From 6e80e20f61fd08bc317d2d699467a39dabe5105d Mon Sep 17 00:00:00 2001 From: rgrp Date: Tue, 13 Dec 2011 11:27:30 +0000 Subject: [PATCH 08/18] [vendor][xs]: trivial rename. --- vendor/{000-1.6.1.min.js => 000-jquery-1.6.1.min.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vendor/{000-1.6.1.min.js => 000-jquery-1.6.1.min.js} (100%) diff --git a/vendor/000-1.6.1.min.js b/vendor/000-jquery-1.6.1.min.js similarity index 100% rename from vendor/000-1.6.1.min.js rename to vendor/000-jquery-1.6.1.min.js From 8b9c76fd29a12a69a0a600d423ac92e2f91724af Mon Sep 17 00:00:00 2001 From: rgrp Date: Thu, 15 Dec 2011 16:13:43 +0000 Subject: [PATCH 09/18] [view/data-explorer][m]: (refs #14) introduce ability to change number of rows viewed. * This also necessitated a nice refactor whereby DataExplorer subviews running off a common Backbone Collection (DocumentList) re-rendering themselves in response to changes in that Collection. --- demo/style/style.css | 10 +++++++++ src/model.js | 9 ++++++-- src/view.js | 51 ++++++++++++++++++++++++++++++-------------- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/demo/style/style.css b/demo/style/style.css index 5e073b3a..40c295f4 100755 --- a/demo/style/style.css +++ b/demo/style/style.css @@ -239,3 +239,13 @@ span.tooltip-status { right: 0px; background: white; } + +.nav-pagination { + display: inline; + float: right; + list-style-type: none; +} + +.nav-pagination input { + width: 40px; +} diff --git a/src/model.js b/src/model.js index 7a783da7..0e73a5d6 100644 --- a/src/model.js +++ b/src/model.js @@ -8,6 +8,10 @@ var my = {}; // A Dataset model. my.Dataset = Backbone.Model.extend({ __type__: 'Dataset', + initialize: function() { + this.currentDocuments = new my.DocumentList(); + }, + getLength: function() { return this.rowCount; }, @@ -19,13 +23,14 @@ my.Dataset = Backbone.Model.extend({ // this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here. // This also illustrates the limitations of separating the Dataset and the Backend getRows: function(numRows, start) { + var self = this; var dfd = $.Deferred(); this.backend.getRows(this.id, numRows, start).then(function(rows) { var docs = _.map(rows, function(row) { return new my.Document(row); }); - var docList = new my.DocumentList(docs); - dfd.resolve(docList); + self.currentDocuments.reset(docs); + dfd.resolve(self.currentDocuments); }); return dfd.promise(); } diff --git a/src/view.js b/src/view.js index e1913d6f..0b2b9ac7 100644 --- a/src/view.js +++ b/src/view.js @@ -16,19 +16,38 @@ my.DataExplorer = Backbone.View.extend({ \ \ \ + \
    \
    \ ', events: { - 'change input[name="nav-toggle"]': 'navChange' + 'change input[name="nav-toggle"]': 'navChange', + 'submit form.display-count': 'displayCountUpdate' }, - initialize: function() { + initialize: function(options) { this.el = $(this.el); + this.config = options.config || {}; + _.extend(this.config, { + displayCount: 10 + }); + this.draw(); + }, + + displayCountUpdate: function(e) { + e.preventDefault(); + this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val()); + this.draw(); + }, + + draw: function() { + var self = this; + this.el.empty(); this.render(); this.$dataViewContainer = this.el.find('.data-view-container'); - var self = this; // retrieve basic data like headers etc // note this.model and dataset returned are the same this.model.fetch().then(function(dataset) { @@ -42,11 +61,13 @@ my.DataExplorer = Backbone.View.extend({ self.flotGraph.el.hide(); self.$dataViewContainer.append(self.dataTable.el) self.$dataViewContainer.append(self.flotGraph.el); + self.model.getRows(self.config.displayCount); }); }, render: function() { - $(this.el).html($(this.template)); + var template = $.mustache(this.template, this.config); + $(this.el).html(template); }, navChange: function(e) { @@ -75,12 +96,11 @@ my.DataTable = Backbone.View.extend({ className: "data-table-container", initialize: function() { - this.el = $(this.el); var self = this; - this.model.getRows().then(function(documentList) { - self._currentDocuments = documentList; - self.render() - }); + this.el = $(this.el); + _.bindAll(this, 'render'); + this.model.currentDocuments.bind('add', this.render); + this.model.currentDocuments.bind('reset', this.render); this.state = {}; // this is nasty. Due to fact that .menu element is not inside this view but is elsewhere in DOM $('.menu li a').live('click', function(e) { @@ -235,7 +255,7 @@ my.DataTable = Backbone.View.extend({ , htmls = $.mustache(template, this.toTemplateJSON()) ; this.el.html(htmls); - this._currentDocuments.forEach(function(doc) { + this.model.currentDocuments.forEach(function(doc) { var tr = $(''); self.el.find('tbody').append(tr); var newView = new my.DataTableRow({ @@ -342,18 +362,17 @@ my.FlotGraph = Backbone.View.extend({ ', initialize: function(options, chart) { + 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.chart = chart; this.chartConfig = { group: null, series: [], graphType: 'line' }; - var self = this; - this.model.getRows().then(function(documentList) { - self._currentDocuments = documentList; - self.render() - }); }, events: { @@ -412,7 +431,7 @@ my.FlotGraph = Backbone.View.extend({ if (this.chartConfig) { $.each(this.chartConfig.series, function (seriesIndex, field) { var points = []; - $.each(self._currentDocuments.models, function (index, doc) { + $.each(self.model.currentDocuments.models, function (index, doc) { var x = doc.get(self.chartConfig.group); var y = doc.get(field); if (typeof x === 'string') { From 51934e1b94d721dde9ba14755e5b968e50c4931d Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 00:26:16 +0000 Subject: [PATCH 10/18] [demo,test][xs]: add id to all rows/documents in fixture data for local backend data. --- demo/js/app.js | 12 ++++++------ test/model.test.js | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/demo/js/app.js b/demo/js/app.js index 54eef6fe..be54fe0f 100755 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -27,12 +27,12 @@ function demoDataset() { var indata = { headers: ['x', 'y', 'z'] , rows: [ - {x: 1, y: 2, z: 3} - , {x: 2, y: 4, z: 6} - , {x: 3, y: 6, z: 9} - , {x: 4, y: 8, z: 12} - , {x: 5, y: 10, z: 15} - , {x: 6, y: 12, z: 18} + {id: 0, x: 1, y: 2, z: 3} + , {id: 1, x: 2, y: 4, z: 6} + , {id: 2, x: 3, y: 6, z: 9} + , {id: 3, x: 4, y: 8, z: 12} + , {id: 4, x: 5, y: 10, z: 15} + , {id: 5, x: 6, y: 12, z: 18} ] }; // this is all rather artificial here but would make more sense with more complex backend diff --git a/test/model.test.js b/test/model.test.js index 12e740bd..c8480672 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -12,12 +12,12 @@ test('new Dataset', function () { var indata = { headers: ['x', 'y', 'z'] , rows: [ - {x: 1, y: 2, z: 3} - , {x: 2, y: 4, z: 6} - , {x: 3, y: 6, z: 9} - , {x: 4, y: 8, z: 12} - , {x: 5, y: 10, z: 15} - , {x: 6, y: 12, z: 18} + {id: 0, x: 1, y: 2, z: 3} + , {id: 1, x: 2, y: 4, z: 6} + , {id: 2, x: 3, y: 6, z: 9} + , {id: 3, x: 4, y: 8, z: 12} + , {id: 4, x: 5, y: 10, z: 15} + , {id: 5, x: 6, y: 12, z: 18} ] }; // this is all rather artificial here but would make more sense with more complex backend From 164da57dfa2ac75d4a8ab585c972741518a66ebc Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 01:00:33 +0000 Subject: [PATCH 11/18] [model/backend][s]: BackendMemory now works with one and only one dataset at a time (plus document what dataset data looks like). * It is clear that if Backend is to handle syncing of Documents we need to only be working with one dataset (or: every Document has to reference a dataset -- in which case Backend and Dataset really are just one object ...). --- demo/js/app.js | 3 +-- src/model.js | 40 ++++++++++++++++++++++++++++------------ test/model.test.js | 3 +-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/demo/js/app.js b/demo/js/app.js index be54fe0f..1bf6b6f6 100755 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -36,8 +36,7 @@ function demoDataset() { ] }; // this is all rather artificial here but would make more sense with more complex backend - var backend = new recline.Model.BackendMemory(); - backend.addDataset({ + var backend = new recline.Model.BackendMemory({ metadata: metadata, data: indata }); diff --git a/src/model.js b/src/model.js index 0e73a5d6..39ff8999 100644 --- a/src/model.js +++ b/src/model.js @@ -54,16 +54,33 @@ my.setBackend = function(backend) { // // Does not need to be a backbone model but provides some conveience my.BackendMemory = Backbone.Model.extend({ - initialize: function() { - this._datasetCache = {} + // Initialize a Backend with a local in-memory dataset. + // + // NB: We can handle one and only one dataset at a time. + // + // :param dataset: the data for a dataset on which operations will be + // performed. In the form of a hash with metadata and data attributes. + // - metadata: hash of key/value attributes of any kind (but usually with title attribute) + // - data: hash with 2 keys: + // - headers: list of header names/labels + // - rows: list of hashes, each hash being one row. A row *must* have an id attribute which is unique. + // + // Example of data: + // + // { + // headers: ['x', 'y', 'z'] + // , rows: [ + // {id: 0, x: 1, y: 2, z: 3} + // , {id: 1, x: 2, y: 4, z: 6} + // ] + // }; + initialize: function(dataset) { + // deep copy + this._datasetAsData = _.extend({}, dataset); }, - // dataset is object with metadata and data attributes - addDataset: function(dataset) { - this._datasetCache[dataset.metadata.id] = dataset; - }, - getDataset: function(id) { + getDataset: function() { var dataset = new my.Dataset({ - id: id + id: this._datasetAsData.metadata.id }); // this is a bit weird but problem is in sync this is set to parent model object so need to give dataset a reference to backend explicitly dataset.backend = this; @@ -76,9 +93,8 @@ my.BackendMemory = Backbone.Model.extend({ // think may make more sense to do work in individual objects rather than in central Backbone.sync if (this.__type__ == 'Dataset') { var dataset = this; - var rawDataset = this.backend._datasetCache[model.id]; + var rawDataset = this.backend._datasetAsData; dataset.set(rawDataset.metadata); - // here we munge it all onto Dataset dataset.set({ headers: rawDataset.data.headers }); @@ -96,7 +112,7 @@ my.BackendMemory = Backbone.Model.extend({ numRows = 10; } var dfd = $.Deferred(); - rows = this._datasetCache[datasetId].data.rows; + rows = this._datasetAsData.data.rows; var results = rows.slice(start, start+numRows); dfd.resolve(results); return dfd.promise(); @@ -105,7 +121,7 @@ my.BackendMemory = Backbone.Model.extend({ // Webstore Backend for connecting to the Webstore // -// Designed to only attached to only dataset and one dataset only ... +// Designed to only attach to one dataset and one dataset only ... // Could generalize to support attaching to different datasets my.BackendWebstore = Backbone.Model.extend({ // require url attribute in initialization data diff --git a/test/model.test.js b/test/model.test.js index c8480672..b27fabf9 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -21,8 +21,7 @@ test('new Dataset', function () { ] }; // this is all rather artificial here but would make more sense with more complex backend - backend = new recline.Model.BackendMemory(); - backend.addDataset({ + backend = new recline.Model.BackendMemory({ metadata: metadata, data: indata }); From 97f50c5bf58f683be6f46c80c976d780e5e7fc56 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 01:36:53 +0000 Subject: [PATCH 12/18] [test/model][s]: update webstore backend test to have ajax fully mocked so tests work fully when offline. --- test/model.test.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/model.test.js b/test/model.test.js index b27fabf9..f2b628b7 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -119,7 +119,6 @@ webstoreData = { }; test('Webstore Backend', function() { - stop(); var backend = new recline.Model.BackendWebstore({ url: 'http://webstore.test.ckan.org/rufuspollock/demo/data' }); @@ -127,9 +126,18 @@ test('Webstore Backend', function() { dataset = backend.getDataset(); var stub = sinon.stub($, 'ajax', function(options) { - return { - then: function(callback) { - callback(webstoreSchema); + if (options.url.indexOf('schema.json') != -1) { + return { + then: function(callback) { + callback(webstoreSchema); + } + } + } else { + console.log('here'); + return { + then: function(callback) { + callback(webstoreData); + } } } }); @@ -137,10 +145,7 @@ test('Webstore Backend', function() { dataset.fetch().then(function(dataset) { deepEqual(['__id__', 'date', 'geometry', 'amount'], dataset.get('headers')); equal(3, dataset.rowCount) - // restore mocked method - $.ajax.restore(); dataset.getRows().then(function(docList) { - start(); equal(3, docList.length) equal("2009-01-01", docList.models[0].get('date')); }); From ce076a2eac6eb8c4b0008cff513fe52e4f6fa87e Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 01:39:31 +0000 Subject: [PATCH 13/18] [model/backend,#6][s]: BackendMemory now supports saving of Documents. --- src/model.js | 28 +++++++++++++++++++++++----- test/model.test.js | 12 +++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/model.js b/src/model.js index 39ff8999..8680fec2 100644 --- a/src/model.js +++ b/src/model.js @@ -36,12 +36,15 @@ my.Dataset = Backbone.Model.extend({ } }); -my.Document = Backbone.Model.extend({}); +my.Document = Backbone.Model.extend({ + __type__: 'Document' +}); my.DocumentList = Backbone.Collection.extend({ + __type__: 'DocumentList', // webStore: new WebStore(this.url), model: my.Document -}) +}); // Backends section // ================ @@ -77,6 +80,7 @@ my.BackendMemory = Backbone.Model.extend({ initialize: function(dataset) { // deep copy this._datasetAsData = _.extend({}, dataset); + _.bindAll(this, 'sync'); }, getDataset: function() { var dataset = new my.Dataset({ @@ -87,13 +91,14 @@ my.BackendMemory = Backbone.Model.extend({ return dataset; }, sync: function(method, model, options) { + var self = this; if (method === "read") { var dfd = $.Deferred(); // this switching on object type is rather horrible // think may make more sense to do work in individual objects rather than in central Backbone.sync - if (this.__type__ == 'Dataset') { - var dataset = this; - var rawDataset = this.backend._datasetAsData; + if (model.__type__ == 'Dataset') { + var dataset = model; + var rawDataset = this._datasetAsData; dataset.set(rawDataset.metadata); dataset.set({ headers: rawDataset.data.headers @@ -102,6 +107,19 @@ my.BackendMemory = Backbone.Model.extend({ dfd.resolve(dataset); } return dfd.promise(); + } else if (method === 'update') { + var dfd = $.Deferred(); + if (model.__type__ == 'Document') { + _.each(this._datasetAsData.data.rows, function(row, idx) { + if(row.id === model.id) { + self._datasetAsData.data.rows[idx] = model.toJSON(); + } + }); + dfd.resolve(model); + } + return dfd.promise(); + } else { + alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model); } }, getRows: function(datasetId, numRows, start) { diff --git a/test/model.test.js b/test/model.test.js index f2b628b7..0234610d 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -27,7 +27,7 @@ test('new Dataset', function () { }); recline.Model.setBackend(backend); var dataset = backend.getDataset(datasetId); - expect(6); + expect(7); dataset.fetch().then(function(dataset) { equal(dataset.get('name'), metadata.name); equal(dataset.get('headers'), indata.headers); @@ -37,8 +37,14 @@ test('new Dataset', function () { }); dataset.getRows().then(function(docList) { equal(docList.length, Math.min(10, indata.rows.length)); - deepEqual(docList.models[0].toJSON(), indata.rows[0]); - }); + var doc1 = docList.models[0]; + deepEqual(doc1.toJSON(), indata.rows[0]); + var newVal = 10; + doc1.set({x: newVal}); + doc1.save().then(function() { + equal(backend._datasetAsData.data.rows[0].x, newVal); + }) + }); }); }); From d5793c40444d82ac0fe839525ff057270e935aee Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 01:46:10 +0000 Subject: [PATCH 14/18] [view][m]: move (disabled) cell editing into DataTableRow view and get it working in proper Backbone way (via save and sync). * [m] when including previous commits and work on Backend --- src/view.js | 87 +++++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/view.js b/src/view.js index 0b2b9ac7..db9a6bb5 100644 --- a/src/view.js +++ b/src/view.js @@ -113,11 +113,7 @@ my.DataTable = Backbone.View.extend({ // see initialize // 'click .menu li': 'onMenuClick', 'click .column-header-menu': 'onColumnHeaderClick', - 'click .row-header-menu': 'onRowHeaderClick', - 'click .data-table-cell-edit': 'onEditClick', - // cell editor - 'click .data-table-cell-editor .okButton': 'onEditorOK', - 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' + 'click .row-header-menu': 'onRowHeaderClick' }, showDialog: function(template, data) { @@ -184,44 +180,6 @@ my.DataTable = Backbone.View.extend({ actions[$(e.target).attr('data-action')](); }, - // ====================================================== - // 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 header = cell.parents('td').attr('data-header'); - var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor)').val(); - // TODO: - alert('Update: ' + header + ' with value ' + newValue + '(But no save as not yet operational'); - return; - var doc = _.find(app.cache, function(cacheDoc) { - return cacheDoc._id === rowId; - }); - doc[header] = newValue; - util.notify("Updating row...", {persist: true, loader: true}); - costco.updateDoc(doc).then(function(response) { - util.notify("Row updated successfully"); - recline.initializeTable(); - }) - }, - - onEditorCancel: function(e) { - var cell = $(e.target).parents('.data-table-cell-value'); - cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); - }, - // ====================================================== // Core Templating template: ' \ @@ -275,6 +233,7 @@ my.DataTable = Backbone.View.extend({ // In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable. my.DataTableRow = Backbone.View.extend({ initialize: function(options) { + _.bindAll(this, 'render'); this._headers = options.headers; this.el = $(this.el); this.model.bind('change', this.render); @@ -290,6 +249,12 @@ my.DataTableRow = Backbone.View.extend({ \ {{/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 doc = this.model; @@ -304,6 +269,42 @@ my.DataTableRow = Backbone.View.extend({ 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 header = cell.parents('td').attr('data-header'); + var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val(); + var newData = {}; + newData[header] = newValue; + this.model.set(newData); + util.notify("Updating row...", {persist: true, loader: true}); + this.model.save().then(function(response) { + util.notify("Row updated successfully"); + }) + .fail(function() { + alert('error saving'); + }); + }, + + onEditorCancel: function(e) { + var cell = $(e.target).parents('.data-table-cell-value'); + cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); } }); From e65bc74a883069629cf06b26d62cdb492bfc1c09 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 12:21:16 +0000 Subject: [PATCH 15/18] [#14,#6,backend,view][m]: delete row in data table now working via Backbone with backend memory. * #6: BackendMemory now supports delete for Documents. --- src/model.js | 11 ++++++++++- src/view.js | 24 ++++++++++++------------ test/model.test.js | 14 +++++++++++--- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/model.js b/src/model.js index 8680fec2..ba143881 100644 --- a/src/model.js +++ b/src/model.js @@ -79,7 +79,7 @@ my.BackendMemory = Backbone.Model.extend({ // }; initialize: function(dataset) { // deep copy - this._datasetAsData = _.extend({}, dataset); + this._datasetAsData = $.extend(true, {}, dataset); _.bindAll(this, 'sync'); }, getDataset: function() { @@ -118,6 +118,15 @@ my.BackendMemory = Backbone.Model.extend({ dfd.resolve(model); } return dfd.promise(); + } else if (method === 'delete') { + var dfd = $.Deferred(); + if (model.__type__ == 'Document') { + this._datasetAsData.data.rows = _.reject(this._datasetAsData.data.rows, function(row) { + return (row.id === model.id); + }); + dfd.resolve(model); + } + return dfd.promise(); } else { alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model); } diff --git a/src/view.js b/src/view.js index db9a6bb5..3beb60f2 100644 --- a/src/view.js +++ b/src/view.js @@ -101,6 +101,7 @@ my.DataTable = Backbone.View.extend({ _.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 is nasty. Due to fact that .menu element is not inside this view but is elsewhere in DOM $('.menu li a').live('click', function(e) { @@ -161,21 +162,20 @@ my.DataTable = Backbone.View.extend({ if (confirm(msg)) costco.deleteColumn(self.state.currentColumn); }, deleteRow: function() { - // TODO: - alert('This function needs to be re-implemented'); - return; - var doc = _.find(app.cache, function(doc) { return doc._id === app.currentRow }); - doc._deleted = true; - costco.uploadDocs([doc]).then( - function(updatedDocs) { + 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); util.notify("Row deleted successfully"); - recline.initializeTable(app.offset); - }, - function(err) { util.notify("Errorz! " + err) } - ) + }) + .fail(function(err) { + util.notify("Errorz! " + err) + }) } } - util.hide('menu'); actions[$(e.target).attr('data-action')](); }, diff --git a/test/model.test.js b/test/model.test.js index 0234610d..e696edcc 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -27,23 +27,32 @@ test('new Dataset', function () { }); recline.Model.setBackend(backend); var dataset = backend.getDataset(datasetId); - expect(7); + expect(9); dataset.fetch().then(function(dataset) { equal(dataset.get('name'), metadata.name); - equal(dataset.get('headers'), indata.headers); + deepEqual(dataset.get('headers'), indata.headers); equal(dataset.getLength(), 6); dataset.getRows(4, 2).then(function(documentList) { deepEqual(indata.rows[2], documentList.models[0].toJSON()); }); dataset.getRows().then(function(docList) { + // Test getRows equal(docList.length, Math.min(10, indata.rows.length)); var doc1 = docList.models[0]; deepEqual(doc1.toJSON(), indata.rows[0]); + + // Test UPDATA var newVal = 10; doc1.set({x: newVal}); doc1.save().then(function() { equal(backend._datasetAsData.data.rows[0].x, newVal); }) + + // Test Delete + doc1.destroy().then(function() { + equal(backend._datasetAsData.data.rows.length, 5); + equal(backend._datasetAsData.data.rows[0].x, indata.rows[1].x); + }); }); }); }); @@ -139,7 +148,6 @@ test('Webstore Backend', function() { } } } else { - console.log('here'); return { then: function(callback) { callback(webstoreData); From 16903be49223beec8b22bd6fdd86cc3cb3795335 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 12:32:07 +0000 Subject: [PATCH 16/18] [src/recline.js][xs]: remove abortive started new code in this file and add deprecation notice. --- src/recline.js | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/src/recline.js b/src/recline.js index 62e4a132..967788b8 100755 --- a/src/recline.js +++ b/src/recline.js @@ -1,41 +1,5 @@ -window.recline = {}; - -recline.Document = Backbone.Model.extend({}); - -recline.DocumentList = Backbone.Collection.extend({ - webStore: new WebStore(this.url), - model: recline.Document -}); - -recline.DataTable = Backbone.View.extend({ - - el: ".data-table-container", - - documents: new recline.DocumentList(this.url), - - // template: TODO ??? - - events: { - - }, - - initialize: function() { - var that = this; - this.documents.fetch({ - success: function(collection, resp) { - that.render() - } - }) - }, - - render: function() { - var template = $( ".dataTableTemplate:first" ).html() - , htmls = $.mustache(template, {rows: this.documents.toJSON()} ) - ; - $(this.el).html(htmls); - return this; - } -}); +// Original Recline code - **Deprecated** +// Left intact while functionality is transferred across to new Backbone setup // var recline = function() { // From b934307a2a1edaaad88de7a4ae4e4710655e0445 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 12:48:16 +0000 Subject: [PATCH 17/18] [tidying][xs]: remove src/backbone-webstore.js as long superseded by Backend stuff in src/model.js. --- src/backbone-webstore.js | 48 ---------------------------------------- 1 file changed, 48 deletions(-) delete mode 100644 src/backbone-webstore.js diff --git a/src/backbone-webstore.js b/src/backbone-webstore.js deleted file mode 100644 index 14d02ae3..00000000 --- a/src/backbone-webstore.js +++ /dev/null @@ -1,48 +0,0 @@ -// replaces `Backbone.sync` with a OKFN webstore based tabular data source - -var WebStore = function(url) { - this.url = url; - this.headers = []; - this.totalRows = 0; - this.getTabularData = function() { - var dfd = $.Deferred(); - var tabularData = { - headers: ['x', 'y', 'z'] - , rows: [ - {x: 1, y: 2, z: 3} - , {x: 2, y: 4, z: 6} - , {x: 3, y: 6, z: 9} - , {x: 4, y: 8, z: 12} - , {x: 5, y: 10, z: 15} - , {x: 6, y: 12, z: 18} - ] - , getLength: function() { return this.rows.length; } - , getRows: function(numRows, start) { - if (start === undefined) { - start = 0; - } - var dfd = $.Deferred(); - var results = this.rows.slice(start, start + numRows); - dfd.resolve(results); - return dfd.promise(); - } - } - dfd.resolve(tabularData); - return dfd.promise(); - } -}; - - -// Override `Backbone.sync` to delegate to the model or collection's -// webStore property, which should be an instance of `WebStore`. -Backbone.sync = function(method, model, options) { - var resp; - var store = model.webStore || model.collection.webStore; - - if (method === "read") { - store.getTabularData().then(function(tabularData) { - tabularData.getRows(10).then(options.success, options.error) - }) - } - -}; \ No newline at end of file From e23354eb4e451bb3171ed89e40a7ebe2e3ca927f Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 15:44:25 +0000 Subject: [PATCH 18/18] [#13,transforms][l]: re-enable column transforms (bulk editing). * TODO: only operates on documents in current view atm -- to do this properly want a transform function on Backend. --- src/costco.js | 3 +- src/view.js | 234 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 234 insertions(+), 3 deletions(-) diff --git a/src/costco.js b/src/costco.js index 7c5b089c..edce7590 100755 --- a/src/costco.js +++ b/src/costco.js @@ -25,6 +25,7 @@ var costco = function() { preview.push({before: JSON.stringify(before), after: JSON.stringify(after)}); } } + // TODO: 2012-01-05 Move this out of this function and up into (view) functions that call this util.render('editPreview', 'expression-preview-container', {rows: preview}); } @@ -166,4 +167,4 @@ var costco = function() { ensureCommit: ensureCommit, uploadCSV: uploadCSV }; -}(); \ No newline at end of file +}(); diff --git a/src/view.js b/src/view.js index 3beb60f2..e416d697 100644 --- a/src/view.js +++ b/src/view.js @@ -147,8 +147,8 @@ my.DataTable = Backbone.View.extend({ var self = this; e.preventDefault(); var actions = { - bulkEdit: function() { self.showDialog('bulkEdit', {name: self.state.currentColumn}) }, - transform: function() { showDialog('transform') }, + bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) }, + transform: function() { self.showTransformDialog('transform') }, csv: function() { window.location.href = app.csvUrl }, json: function() { window.location.href = "_rewrite/api/json" }, urlImport: function() { showDialog('urlImport') }, @@ -180,6 +180,37 @@ my.DataTable = Backbone.View.extend({ 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 my.DataTransform({ + }); + view.render(); + $el.empty(); + $el.append(view.el); + util.observeExit($el, function() { + util.hide('dialog'); + }) + $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + }, + + // ====================================================== // Core Templating template: ' \ @@ -308,6 +339,205 @@ my.DataTableRow = Backbone.View.extend({ } }); + +// View (Dialog) for doing data transformations (on columns of data). +my.ColumnTransform = Backbone.View.extend({ + className: 'transform-column-view', + template: ' \ +
    \ + Functional transform on column {{name}} \ +
    \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
    \ + Expression \ +
    \ +
    \ + \ +
    \ +
    \ + No syntax error. \ +
    \ +
    \ + Preview \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ + \ + ', + + events: { + 'click .okButton': 'onSubmit' + , 'keydown .expression-preview-code': 'onEditorKeydown' + }, + + initialize: function() { + this.el = $(this.el); + }, + + render: function() { + var htmls = $.mustache(this.template, + {name: this.state.currentColumn} + ) + this.el.html(htmls); + // Put in the basic (identity) transform script + // TODO: put this into the template? + var editor = this.el.find('.expression-preview-code'); + editor.val("function(doc) {\n doc['"+ this.state.currentColumn+"'] = doc['"+ this.state.currentColumn+"'];\n return doc;\n}"); + editor.focus().get(0).setSelectionRange(18, 18); + editor.keydown(); + }, + + onSubmit: function(e) { + var self = this; + var funcText = this.el.find('.expression-preview-code').val(); + var editFunc = costco.evalFunction(funcText); + if (editFunc.errorMessage) { + util.notify("Error with function! " + editFunc.errorMessage); + return; + } + util.hide('dialog'); + util.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true}); + var docs = self.model.currentDocuments.map(function(doc) { + return doc.toJSON(); + }); + // TODO: notify about failed docs? + var toUpdate = costco.mapDocs(docs, editFunc).edited; + var totalToUpdate = toUpdate.length; + function onCompletedUpdate() { + totalToUpdate += -1; + if (totalToUpdate === 0) { + util.notify(toUpdate.length + " documents updated successfully"); + alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)'); + self.remove(); + } + } + // TODO: Very inefficient as we search through all docs every time! + _.each(toUpdate, function(editedDoc) { + var realDoc = self.model.currentDocuments.get(editedDoc.id); + realDoc.set(editedDoc); + realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate) + }); + }, + + onEditorKeydown: function(e) { + var self = this; + // if you don't setTimeout it won't grab the latest character if you call e.target.value + window.setTimeout( function() { + var errors = self.el.find('.expression-preview-parsing-status'); + var editFunc = costco.evalFunction(e.target.value); + if (!editFunc.errorMessage) { + errors.text('No syntax error.'); + var docs = self.model.currentDocuments.map(function(doc) { + return doc.toJSON(); + }); + costco.previewTransform(docs, editFunc, self.state.currentColumn); + } else { + errors.text(editFunc.errorMessage); + } + }, 1, true); + } +}); + +// View (Dialog) for doing data transformations on whole dataset. +my.DataTransform = Backbone.View.extend({ + className: 'transform-view', + template: ' \ +
    \ + Recursive transform on all rows \ +
    \ +
    \ +
    \ +

    Traverse and transform objects by visiting every node on a recursive walk using js-traverse.

    \ + \ + \ + \ + \ + \ + \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
    \ + Expression \ +
    \ +
    \ + \ +
    \ +
    \ + No syntax error. \ +
    \ +
    \ + Preview \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ +
    \ + \ + ', + + initialize: function() { + this.el = $(this.el); + }, + + render: function() { + this.el.html(this.template); + } +}); + + my.FlotGraph = Backbone.View.extend({ tagName: "div",