Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock 2012-01-05 16:11:07 +00:00
commit e57340fa56
12 changed files with 640 additions and 238 deletions

View File

@ -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 2011 Max Ogden and Rufus Pollock.
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.

View File

@ -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);
@ -27,21 +27,20 @@ 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
var backend = new recline.BackendMemory();
backend.addDataset({
var backend = new recline.Model.BackendMemory({
metadata: metadata,
data: indata
});
recline.setBackend(backend);
recline.Model.setBackend(backend);
var dataset = backend.getDataset(datasetId);
return dataset;
}
@ -54,10 +53,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);
});

View File

@ -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;
}

View File

@ -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)
})
}
};

View File

@ -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
};
}();
}();

View File

@ -1,61 +1,105 @@
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',
initialize: function() {
this.currentDocuments = new my.DocumentList();
},
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 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);
});
self.currentDocuments.reset(docs);
dfd.resolve(self.currentDocuments);
});
return dfd.promise();
}
});
recline.Document = Backbone.Model.extend({});
my.Document = Backbone.Model.extend({
__type__: 'Document'
});
recline.DocumentList = Backbone.Collection.extend({
my.DocumentList = Backbone.Collection.extend({
__type__: 'DocumentList',
// 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({
initialize: function() {
this._datasetCache = {}
my.BackendMemory = Backbone.Model.extend({
// 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(true, {}, dataset);
_.bindAll(this, 'sync');
},
// dataset is object with metadata and data attributes
addDataset: function(dataset) {
this._datasetCache[dataset.metadata.id] = dataset;
},
getDataset: function(id) {
var dataset = new recline.Dataset({
id: id
getDataset: function() {
var dataset = new my.Dataset({
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;
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._datasetCache[model.id];
if (model.__type__ == 'Dataset') {
var dataset = model;
var rawDataset = this._datasetAsData;
dataset.set(rawDataset.metadata);
// here we munge it all onto Dataset
dataset.set({
headers: rawDataset.data.headers
});
@ -63,6 +107,28 @@ recline.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 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);
}
},
getRows: function(datasetId, numRows, start) {
@ -73,7 +139,7 @@ recline.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();
@ -82,15 +148,15 @@ recline.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
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;
@ -146,3 +212,8 @@ recline.BackendWebstore = Backbone.Model.extend({
return dfd.promise();
}
});
return my;
}(jQuery);

View File

@ -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() {
//

View File

@ -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: ' \
@ -11,37 +16,58 @@ recline.DataExplorer = Backbone.View.extend({
<input type="radio" id="nav-graph" name="nav-toggle" value="graph" /> \
<label for="nav-graph">Graph</label> \
</span> \
<ul class="nav-pagination"> \
<li><form class="display-count"><label for="per-page">Display count</label> <input name="displayCount" type="text" value="{{displayCount}}" /></form></li> \
</ul> \
</div> \
<div class="data-view-container"></div> \
',
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) {
// 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();
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) {
@ -62,17 +88,20 @@ recline.DataExplorer = Backbone.View.extend({
}
});
recline.DataTable = Backbone.View.extend({
// DataTable provides a tabular view on a Dataset.
//
// Initialize it with a recline.Dataset object.
my.DataTable = Backbone.View.extend({
tagName: "div",
className: "data-table-container",
initialize: function() {
this.el = $(this.el);
var self = this;
this.model.getRows().then(function(rows) {
self._currentRows = rows;
self.render()
});
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 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) {
@ -85,11 +114,7 @@ recline.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) {
@ -122,8 +147,8 @@ recline.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') },
@ -137,25 +162,146 @@ recline.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')]();
},
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: ' \
<table class="data-table" cellspacing="0"> \
<thead> \
<tr> \
{{#notEmpty}}<td class="column-header"></td>{{/notEmpty}} \
{{#headers}} \
<td class="column-header"> \
<div class="column-header-title"> \
<a class="column-header-menu"></a> \
<span class="column-header-name">{{.}}</span> \
</div> \
</div> \
</td> \
{{/headers}} \
</tr> \
</thead> \
<tbody></tbody> \
</table> \
',
toTemplateJSON: function() {
var modelData = this.model.toJSON()
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.model.currentDocuments.forEach(function(doc) {
var tr = $('<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;
}
});
// 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.
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);
},
template: ' \
<td><a class="row-header-menu"></a></td> \
{{#cells}} \
<td data-header="{{header}}"> \
<div class="data-table-cell-content"> \
<a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell">&nbsp;</a> \
<div class="data-table-cell-value">{{value}}</div> \
</div> \
</td> \
{{/cells}} \
',
events: {
'click .data-table-cell-edit': 'onEditClick',
// cell editor
'click .data-table-cell-editor .okButton': 'onEditorOK',
'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
},
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;
},
// ======================================================
// Cell Editor
@ -174,50 +320,225 @@ recline.DataTable = Backbone.View.extend({
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;
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});
costco.updateDoc(doc).then(function(response) {
util.notify("Row updated successfully");
recline.initializeTable();
})
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");
},
// ======================================================
// Core Templating
toTemplateJSON: function() {
var modelData = this.model.toJSON()
modelData.rows = _.map(this._currentRows, function(row) {
var cellData = _.map(modelData.headers, function(header) {
return {header: header, value: row[header]}
})
return { id: 'xxx', cells: cellData }
})
modelData.notEmpty = ( modelData.headers.length > 0 )
return modelData;
},
render: function() {
var template = $( ".dataTableTemplate:first" ).html()
, htmls = $.mustache(template, this.toTemplateJSON())
;
$(this.el).html(htmls);
return this;
}
});
recline.FlotGraph = Backbone.View.extend({
// View (Dialog) for doing data transformations (on columns of data).
my.ColumnTransform = Backbone.View.extend({
className: 'transform-column-view',
template: ' \
<div class="dialog-header"> \
Functional transform on column {{name}} \
</div> \
<div class="dialog-body"> \
<div class="grid-layout layout-tight layout-full"> \
<table> \
<tbody> \
<tr> \
<td colspan="4"> \
<div class="grid-layout layout-tight layout-full"> \
<table rows="4" cols="4"> \
<tbody> \
<tr style="vertical-align: bottom;"> \
<td colspan="4"> \
Expression \
</td> \
</tr> \
<tr> \
<td colspan="3"> \
<div class="input-container"> \
<textarea class="expression-preview-code"></textarea> \
</div> \
</td> \
<td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
No syntax error. \
</td> \
</tr> \
<tr> \
<td colspan="4"> \
<div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
<span>Preview</span> \
<div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
<div class="expression-preview-container" style="width: 652px; "> \
</div> \
</div> \
</div> \
</td> \
</tr> \
</tbody> \
</table> \
</div> \
</td> \
</tr> \
</tbody> \
</table> \
</div> \
</div> \
<div class="dialog-footer"> \
<button class="okButton button">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
<button class="cancelButton button">Cancel</button> \
</div> \
',
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: ' \
<div class="dialog-header"> \
Recursive transform on all rows \
</div> \
<div class="dialog-body"> \
<div class="grid-layout layout-full"> \
<p class="info">Traverse and transform objects by visiting every node on a recursive walk using <a href="https://github.com/substack/js-traverse">js-traverse</a>.</p> \
<table> \
<tbody> \
<tr> \
<td colspan="4"> \
<div class="grid-layout layout-tight layout-full"> \
<table rows="4" cols="4"> \
<tbody> \
<tr style="vertical-align: bottom;"> \
<td colspan="4"> \
Expression \
</td> \
</tr> \
<tr> \
<td colspan="3"> \
<div class="input-container"> \
<textarea class="expression-preview-code"></textarea> \
</div> \
</td> \
<td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
No syntax error. \
</td> \
</tr> \
<tr> \
<td colspan="4"> \
<div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
<span>Preview</span> \
<div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
<div class="expression-preview-container" style="width: 652px; "> \
</div> \
</div> \
</div> \
</td> \
</tr> \
</tbody> \
</table> \
</div> \
</td> \
</tr> \
</tbody> \
</table> \
</div> \
</div> \
<div class="dialog-footer"> \
<button class="okButton button">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
<button class="cancelButton button">Cancel</button> \
</div> \
',
initialize: function() {
this.el = $(this.el);
},
render: function() {
this.el.html(this.template);
}
});
my.FlotGraph = Backbone.View.extend({
tagName: "div",
className: "data-graph-container",
@ -272,18 +593,17 @@ recline.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(rows) {
self._currentRows = rows;
self.render()
});
},
events: {
@ -342,8 +662,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.model.currentDocuments.models, function (index, doc) {
var x = doc.get(self.chartConfig.group);
var y = doc.get(field);
if (typeof x === 'string') {
x = index;
}
@ -370,3 +691,8 @@ recline.FlotGraph = Backbone.View.extend({
return this;
}
});
return my;
}(jQuery);

View File

@ -12,6 +12,8 @@
<script type="text/javascript" src="../src/model.js"></script>
<script type="text/javascript" src="model.test.js"></script>
<script type="text/javascript" src="../src/view.js"></script>
<script type="text/javascript" src="view.test.js"></script>
</head>
<body>
<h1 id="qunit-header">Qunit Tests</h1>
@ -19,9 +21,9 @@
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">
<div id="our-dialog">
</div>
<div class="fixtures">
<table class="test-datatable">
</table>
</div>
</body>
</html>

View File

@ -12,34 +12,48 @@ 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
backend = new recline.BackendMemory();
backend.addDataset({
backend = new recline.Model.BackendMemory({
metadata: metadata,
data: indata
});
recline.setBackend(backend);
recline.Model.setBackend(backend);
var dataset = backend.getDataset(datasetId);
expect(6);
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(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) {
// 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);
});
});
});
});
@ -120,30 +134,34 @@ 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) {
return {
then: function(callback) {
callback(webstoreSchema);
if (options.url.indexOf('schema.json') != -1) {
return {
then: function(callback) {
callback(webstoreSchema);
}
}
} else {
return {
then: function(callback) {
callback(webstoreData);
}
}
}
});
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) {
start();
equal(3,rows.length)
equal("2009-01-01", rows[0].date);
dataset.getRows().then(function(docList) {
equal(3, docList.length)
equal("2009-01-01", docList.models[0].get('date'));
});
});
});

25
test/view.test.js Normal file
View File

@ -0,0 +1,25 @@
(function ($) {
module("View");
test('new DataTableRow View', function () {
var $el = $('<tr />');
$('.fixtures .test-datatable').append($el);
var doc = new recline.Model.Document({
'id': 1,
'b': '2',
'a': '1'
});
var view = new recline.View.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);