Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock
2012-04-01 16:48:34 +01:00
12 changed files with 373 additions and 120 deletions

View File

@@ -60,6 +60,10 @@
clear: both; clear: both;
} }
.recline-query-facet-editor {
clear: both;
}
/********************************************************** /**********************************************************
* Notifications * Notifications
*********************************************************/ *********************************************************/

View File

@@ -81,19 +81,20 @@ function localDataset() {
, name: '1-my-test-dataset' , name: '1-my-test-dataset'
, id: datasetId , id: datasetId
}, },
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'label'}], fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
documents: [ documents: [
{id: 0, x: 1, y: 2, z: 3, label: 'first'} {id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
, {id: 1, x: 2, y: 4, z: 6, label: 'second'} , {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
, {id: 2, x: 3, y: 6, z: 9, label: 'third'} , {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
, {id: 3, x: 4, y: 8, z: 12, label: 'fourth'} , {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth'}
, {id: 4, x: 5, y: 10, z: 15, label: 'fifth'} , {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth'}
, {id: 5, x: 6, y: 12, z: 18, label: 'sixth'} , {id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth'}
] ]
}; };
var backend = new recline.Backend.Memory(); var backend = new recline.Backend.Memory();
backend.addDataset(inData); backend.addDataset(inData);
var dataset = new recline.Model.Dataset({id: datasetId}, backend); var dataset = new recline.Model.Dataset({id: datasetId}, backend);
dataset.queryState.addFacet('country');
return dataset; return dataset;
} }

View File

@@ -9,9 +9,9 @@
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--> <![endif]-->
<link rel="stylesheet" href="vendor/bootstrap/2.0.0/css/bootstrap.css" /> <link rel="stylesheet" href="vendor/bootstrap/2.0.2/css/bootstrap.css" />
<link rel="stylesheet" href="http://opendatahandbook.org/en/_static/bootstrap-sphinx.css" /> <link rel="stylesheet" href="http://opendatahandbook.org/en/_static/bootstrap-sphinx.css" />
<link rel="stylesheet" href="vendor/bootstrap/2.0.0/css/bootstrap-responsive.css" /> <link rel="stylesheet" href="vendor/bootstrap/2.0.2/css/bootstrap-responsive.css" />
<style type="text/css"> <style type="text/css">
html, body { html, body {
@@ -65,7 +65,6 @@
<ul class="nav"> <ul class="nav">
<li><a href="demo/">Demo</a></li> <li><a href="demo/">Demo</a></li>
<li><a href="#docs">Docs</a></li> <li><a href="#docs">Docs</a></li>
<li><a href="#downloads">Download</a></li>
<li><a href="http://github.com/okfn/recline/">Code on GitHub</a></li> <li><a href="http://github.com/okfn/recline/">Code on GitHub</a></li>
</ul> </ul>
<a class="nav-logo pull-right" href="http://okfn.org/" title="An Open Knowledge Foundation Project"> <a class="nav-logo pull-right" href="http://okfn.org/" title="An Open Knowledge Foundation Project">
@@ -140,7 +139,7 @@ Backbone.history.start();
href="demo/">Demo</a> -- just hit view source (NB: the javascript for the href="demo/">Demo</a> -- just hit view source (NB: the javascript for the
demo is in: <a href="demo/js/app.js">app.js</a>).</p> demo is in: <a href="demo/js/app.js">app.js</a>).</p>
<h3 id="doc-concepts">Structure</h3> <h3 id="docs-concepts">Concepts and Structure</h3>
<p>Recline has a simple structure layered on top of the basic Model/View <p>Recline has a simple structure layered on top of the basic Model/View
distinction inherent in Backbone.</p> distinction inherent in Backbone.</p>
@@ -154,8 +153,12 @@ Backbone.history.start();
<p><strong>Backends</strong> connect Dataset and Documents to data <p><strong>Backends</strong> connect Dataset and Documents to data
from a specific 'Backend' data source. They provide methods for loading and 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 saving Datasets and individuals Documents as well as for bulk loading via a
query API and doing bulk transforms on the backend. More <a query API and doing bulk transforms on the backend.</p>
href="#doc-backends">info on backends can be found below</a>.</p> <p>A template Base class can be found <a href="docs/backend/base.html">in
the Backend base module of the source docs</a>. It documents both the
relevant methods a Backend must have and (optionally) provides a base
'class' for inheritance. You can also find detailed examples of backend
implementations in the source documentation below.</p>
<p><strong>Views</strong>: complementing the model are various Views (you can also easily write your own). Each view holds a pointer to a Dataset:</p> <p><strong>Views</strong>: complementing the model are various Views (you can also easily write your own). Each view holds a pointer to a Dataset:</p>
<ul> <ul>
@@ -164,58 +167,16 @@ Backbone.history.start();
<li>FlotGraph: a simple graphing view using <a href="http://code.google.com/p/flot/">Flot</a>.</li> <li>FlotGraph: a simple graphing view using <a href="http://code.google.com/p/flot/">Flot</a>.</li>
</ul> </ul>
<h3 id="doc-backends">Backends</h3> <h3 id="docs-source">Source Docs (via Docco)</h3>
<p>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.</p>
<p>A backend <em>must</em> implement two methods:</p>
<pre>
sync(method, model, options)
query(dataset, queryObj)
</pre>
<h4>sync(method, model, options)</h4>
<p>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.</p>
<p>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.</p>
<p>For backends supporting write operations you must implement update and
delete support for Document objects.</p>
<p>All code paths should return an object conforming to the jquery promise
API.</p>
<h4>query(dataset, queryObj)</h4>
<p>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.</p>
<p><code>queryObj</code> 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 <a href="http://github.com/okfn/recline/issues/34">issue 34</a>. (Of
course, if you are writing your own backend, and hence have control over the
interpretation of the query object, you can use whatever structure you
like).</p>
<h3>Source Docs (via Docco)</h3>
<ul> <ul>
<li><a href="docs/model.html">Models</a></li> <li><a href="docs/model.html">Models</a></li>
<li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li> <li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li>
<li><a href="docs/view-grid.html">DataGrid View</a></li> <li><a href="docs/view-grid.html">DataGrid View</a></li>
<li><a href="docs/view-flot-graph.html">Graph View (based on Flot)</a></li> <li><a href="docs/view-flot-graph.html">Graph View (based on Flot)</a></li>
<li><a href="docs/backend/base.html">Backend: Base (base class providing a template for backends)</a></li>
<li><a href="docs/backend/memory.html">Backend: Memory (local data)</a></li> <li><a href="docs/backend/memory.html">Backend: Memory (local data)</a></li>
<li><a href="docs/backend/elasticsearch.html">Backend: ElasticSearch</a></li> <li><a href="docs/backend/elasticsearch.html">Backend: ElasticSearch</a></li>
<li><a href="docs/backend/dataproxy.html">Backend: DataProxy</a></li> <li><a href="docs/backend/dataproxy.html">Backend: DataProxy (CSV and XLS on the Web)</a></li>
<li><a href="docs/backend/gdocs.html">Backend: Google Docs (Spreadsheet)</a></li> <li><a href="docs/backend/gdocs.html">Backend: Google Docs (Spreadsheet)</a></li>
</ul> </ul>
@@ -258,7 +219,7 @@ like).</p>
<ul class="nav nav-list"> <ul class="nav nav-list">
<li><a href="#docs-using">Using it</a></li> <li><a href="#docs-using">Using it</a></li>
<li><a href="#docs-concepts">Concepts and Structure</a></li> <li><a href="#docs-concepts">Concepts and Structure</a></li>
<li><a href="#docs-backends">Backends</a></li> <li><a href="#docs-source">Source Docs (Docco)</a></li>
</ul> </ul>
</div><!--/.well --> </div><!--/.well -->
</div><!--/span--> </div><!--/span-->

View File

@@ -2,7 +2,7 @@
// //
// Backends are connectors to backend data sources and stores // Backends are connectors to backend data sources and stores
// //
// This is just the base module containing various convenience methods. // This is just the base module containing a template Base class and convenience methods.
this.recline = this.recline || {}; this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {}; this.recline.Backend = this.recline.Backend || {};
@@ -14,12 +14,91 @@ this.recline.Backend = this.recline.Backend || {};
return model.backend.sync(method, model, options); return model.backend.sync(method, model, options);
} }
// ## wrapInTimeout // ## recline.Backend.Base
// //
// Crude way to catch backend errors // Base class for backends providing a template and convenience functions.
// You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.
//
// Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
my.Base = Backbone.Model.extend({
// ### sync
//
// An implementation of Backbone.sync that will be 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.
sync: function(method, model, options) {
},
// ### query
//
// 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.
//
// @param {recline.model.Dataset} model: Dataset model.
//
// @param {Object} queryObj: object describing a query (usually produced by
// using recline.Model.Query and calling toJSON on it).
//
// The structure of data in the Query object or
// Hash should follow that defined in <a
// href="http://github.com/okfn/recline/issues/34">issue 34</a>.
// (Of course, if you are writing your own backend, and hence
// have control over the interpretation of the query object, you
// can use whatever structure you like).
//
// @returns {Promise} promise API object. The promise resolve method will
// be called on query completion with a QueryResult object.
//
// A QueryResult has the following structure (modelled closely on
// ElasticSearch - see <a
// href="https://github.com/okfn/recline/issues/57">this issue for more
// details</a>):
//
// <pre>
// {
// total: // (required) total number of results (can be null)
// hits: [ // (required) one entry for each result document
// {
// _score: // (optional) match score for document
// _type: // (optional) document type
// _source: // (required) document/row object
// }
// ],
// facets: { // (optional)
// // facet results (as per <http://www.elasticsearch.org/guide/reference/api/search/facets/>)
// }
// }
// </pre>
query: function(model, queryObj) {
},
// convenience method to convert simple set of documents / rows to a QueryResult
_docsToQueryResult: function(rows) {
var hits = _.map(rows, function(row) {
return { _source: row };
});
return {
total: null,
hits: hits
};
},
// ## _wrapInTimeout
//
// Convenience method providing a crude way to catch backend errors on JSONP calls.
// Many of backends use JSONP and so will not get error messages and this is // Many of backends use JSONP and so will not get error messages and this is
// a crude way to catch those errors. // a crude way to catch those errors.
my.wrapInTimeout = function(ourFunction) { _wrapInTimeout: function(ourFunction) {
var dfd = $.Deferred(); var dfd = $.Deferred();
var timeout = 5000; var timeout = 5000;
var timer = setTimeout(function() { var timer = setTimeout(function() {
@@ -38,5 +117,7 @@ this.recline.Backend = this.recline.Backend || {};
; ;
return dfd.promise(); return dfd.promise();
} }
});
}(jQuery, this.recline.Backend)); }(jQuery, this.recline.Backend));

View File

@@ -16,7 +16,7 @@ this.recline.Backend = this.recline.Backend || {};
// * format: (optional) csv | xls (defaults to csv if not specified) // * format: (optional) csv | xls (defaults to csv if not specified)
// //
// Note that this is a **read-only** backend. // Note that this is a **read-only** backend.
my.DataProxy = Backbone.Model.extend({ my.DataProxy = my.Base.extend({
defaults: { defaults: {
dataproxy_url: 'http://jsonpdataproxy.appspot.com' dataproxy_url: 'http://jsonpdataproxy.appspot.com'
}, },
@@ -35,6 +35,7 @@ this.recline.Backend = this.recline.Backend || {};
} }
}, },
query: function(dataset, queryObj) { query: function(dataset, queryObj) {
var self = this;
var base = this.get('dataproxy_url'); var base = this.get('dataproxy_url');
var data = { var data = {
url: dataset.get('url') url: dataset.get('url')
@@ -47,7 +48,7 @@ this.recline.Backend = this.recline.Backend || {};
, dataType: 'jsonp' , dataType: 'jsonp'
}); });
var dfd = $.Deferred(); var dfd = $.Deferred();
my.wrapInTimeout(jqxhr).done(function(results) { this._wrapInTimeout(jqxhr).done(function(results) {
if (results.error) { if (results.error) {
dfd.reject(results.error); dfd.reject(results.error);
} }
@@ -62,7 +63,7 @@ this.recline.Backend = this.recline.Backend || {};
}); });
return tmp; return tmp;
}); });
dfd.resolve(_out); dfd.resolve(self._docsToQueryResult(_out));
}) })
.fail(function(arguments) { .fail(function(arguments) {
dfd.reject(arguments); dfd.reject(arguments);

View File

@@ -19,7 +19,7 @@ this.recline.Backend = this.recline.Backend || {};
// localhost:9200 with index twitter and type tweet it would be // localhost:9200 with index twitter and type tweet it would be
// //
// <pre>http://localhost:9200/twitter/tweet</pre> // <pre>http://localhost:9200/twitter/tweet</pre>
my.ElasticSearch = Backbone.Model.extend({ my.ElasticSearch = my.Base.extend({
_getESUrl: function(dataset) { _getESUrl: function(dataset) {
var out = dataset.get('elasticsearch_url'); var out = dataset.get('elasticsearch_url');
if (out) return out; if (out) return out;
@@ -39,7 +39,7 @@ this.recline.Backend = this.recline.Backend || {};
dataType: 'jsonp' dataType: 'jsonp'
}); });
var dfd = $.Deferred(); var dfd = $.Deferred();
my.wrapInTimeout(jqxhr).done(function(schema) { this._wrapInTimeout(jqxhr).done(function(schema) {
// only one top level key in ES = the type so we can ignore it // only one top level key in ES = the type so we can ignore it
var key = _.keys(schema)[0]; var key = _.keys(schema)[0];
var fieldData = _.map(schema[key].properties, function(dict, fieldName) { var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
@@ -93,13 +93,15 @@ this.recline.Backend = this.recline.Backend || {};
var dfd = $.Deferred(); var dfd = $.Deferred();
// TODO: fail case // TODO: fail case
jqxhr.done(function(results) { jqxhr.done(function(results) {
model.docCount = results.hits.total; _.each(results.hits.hits, function(hit) {
var docs = _.map(results.hits.hits, function(result) { if (!'id' in hit._source && hit._id) {
var _out = result._source; hit._source.id = hit._id;
_out.id = result._id; }
return _out; })
}); if (results.facets) {
dfd.resolve(docs); results.hits.facets = results.facets;
}
dfd.resolve(results.hits);
}); });
return dfd.promise(); return dfd.promise();
} }

View File

@@ -16,7 +16,7 @@ this.recline.Backend = this.recline.Backend || {};
// 'gdocs' // 'gdocs'
// ); // );
// </pre> // </pre>
my.GDoc = Backbone.Model.extend({ my.GDoc = my.Base.extend({
getUrl: function(dataset) { getUrl: function(dataset) {
var url = dataset.get('url'); var url = dataset.get('url');
if (url.indexOf('feeds/list') != -1) { if (url.indexOf('feeds/list') != -1) {
@@ -67,7 +67,7 @@ this.recline.Backend = this.recline.Backend || {};
_.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; }) _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
return obj; return obj;
}); });
dfd.resolve(objs); dfd.resolve(this._docsToQueryResult(objs));
return dfd; return dfd;
}, },
gdocsToJavascript: function(gdocsSpreadsheet) { gdocsToJavascript: function(gdocsSpreadsheet) {

View File

@@ -29,8 +29,8 @@ this.recline.Backend = this.recline.Backend || {};
datasetInfo.fields = fields; datasetInfo.fields = fields;
} else { } else {
if (data) { if (data) {
datasetInfo.fields = _.map(data[0], function(cell) { datasetInfo.fields = _.map(data[0], function(value, key) {
return {id: cell}; return {id: key};
}); });
} }
} }
@@ -69,7 +69,7 @@ this.recline.Backend = this.recline.Backend || {};
// dataset.fetch(); // dataset.fetch();
// etc ... // etc ...
// </pre> // </pre>
my.Memory = Backbone.Model.extend({ my.Memory = my.Base.extend({
initialize: function() { initialize: function() {
this.datasets = {}; this.datasets = {};
}, },
@@ -115,9 +115,10 @@ this.recline.Backend = this.recline.Backend || {};
} }
}, },
query: function(model, queryObj) { query: function(model, queryObj) {
var dfd = $.Deferred();
var out = {};
var numRows = queryObj.size; var numRows = queryObj.size;
var start = queryObj.from; var start = queryObj.from;
var dfd = $.Deferred();
results = this.datasets[model.id].documents; results = this.datasets[model.id].documents;
// not complete sorting! // not complete sorting!
_.each(queryObj.sort, function(sortObj) { _.each(queryObj.sort, function(sortObj) {
@@ -127,9 +128,48 @@ this.recline.Backend = this.recline.Backend || {};
return (sortObj[fieldName].order == 'asc') ? _out : -1*_out; return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
}); });
}); });
var results = results.slice(start, start+numRows); out.facets = this._computeFacets(results, queryObj);
dfd.resolve(results); var total = results.length;
resultsObj = this._docsToQueryResult(results.slice(start, start+numRows));
_.extend(out, resultsObj);
out.total = total;
dfd.resolve(out);
return dfd.promise(); return dfd.promise();
},
_computeFacets: function(documents, queryObj) {
var facetResults = {};
if (!queryObj.facets) {
return facetsResults;
}
_.each(queryObj.facets, function(query, facetId) {
facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
facetResults[facetId].termsall = {};
});
// faceting
_.each(documents, function(doc) {
_.each(queryObj.facets, function(query, facetId) {
var fieldId = query.terms.field;
var val = doc[fieldId];
var tmp = facetResults[facetId];
if (val) {
tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
} else {
tmp.missing = tmp.missing + 1;
}
});
});
_.each(queryObj.facets, function(query, facetId) {
var tmp = facetResults[facetId];
var terms = _.map(tmp.termsall, function(count, term) {
return { term: term, count: count };
});
tmp.terms = _.sortBy(terms, function(item) {
// want descending order
return -item.count;
});
});
return facetResults;
} }
}); });
recline.Model.backends['memory'] = new my.Memory(); recline.Model.backends['memory'] = new my.Memory();

View File

@@ -23,9 +23,11 @@ my.Dataset = Backbone.Model.extend({
} }
this.fields = new my.FieldList(); this.fields = new my.FieldList();
this.currentDocuments = new my.DocumentList(); this.currentDocuments = new my.DocumentList();
this.facets = new my.FacetList();
this.docCount = null; this.docCount = null;
this.queryState = new my.Query(); this.queryState = new my.Query();
this.queryState.bind('change', this.query); this.queryState.bind('change', this.query);
this.queryState.bind('facet:add', this.query);
}, },
// ### query // ### query
@@ -38,18 +40,26 @@ my.Dataset = Backbone.Model.extend({
// Resulting DocumentList are used to reset this.currentDocuments and are // Resulting DocumentList are used to reset this.currentDocuments and are
// also returned. // also returned.
query: function(queryObj) { query: function(queryObj) {
this.trigger('query:start');
var self = this; var self = this;
this.queryState.set(queryObj); this.trigger('query:start');
var actualQuery = self._prepareQuery(queryObj);
var dfd = $.Deferred(); var dfd = $.Deferred();
this.backend.query(this, this.queryState.toJSON()).done(function(rows) { this.backend.query(this, actualQuery).done(function(queryResult) {
var docs = _.map(rows, function(row) { self.docCount = queryResult.total;
var _doc = new my.Document(row); var docs = _.map(queryResult.hits, function(hit) {
var _doc = new my.Document(hit._source);
_doc.backend = self.backend; _doc.backend = self.backend;
_doc.dataset = self; _doc.dataset = self;
return _doc; return _doc;
}); });
self.currentDocuments.reset(docs); self.currentDocuments.reset(docs);
if (queryResult.facets) {
var facets = _.map(queryResult.facets, function(facetResult, facetId) {
facetResult.id = facetId;
return new my.Facet(facetResult);
});
self.facets.reset(facets);
}
self.trigger('query:done'); self.trigger('query:done');
dfd.resolve(self.currentDocuments); dfd.resolve(self.currentDocuments);
}) })
@@ -60,6 +70,14 @@ my.Dataset = Backbone.Model.extend({
return dfd.promise(); return dfd.promise();
}, },
_prepareQuery: function(newQueryObj) {
if (newQueryObj) {
this.queryState.set(newQueryObj);
}
var out = this.queryState.toJSON();
return out;
},
toTemplateJSON: function() { toTemplateJSON: function() {
var data = this.toJSON(); var data = this.toJSON();
data.docCount = this.docCount; data.docCount = this.docCount;
@@ -116,7 +134,45 @@ my.Query = Backbone.Model.extend({
defaults: { defaults: {
size: 100 size: 100
, from: 0 , from: 0
, facets: {}
},
// Set (update or add) a terms filter
// http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html
setFilter: function(fieldId, values) {
},
addFacet: function(fieldId) {
var facets = this.get('facets');
// Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)
if (_.contains(_.keys(facets), fieldId)) {
return;
} }
facets[fieldId] = {
terms: { field: fieldId }
};
this.set({facets: facets}, {silent: true});
this.trigger('facet:add', this);
}
});
// ## A Facet (Result)
my.Facet = Backbone.Model.extend({
defaults: {
_type: 'terms',
// total number of tokens in the facet
total: 0,
// number of facet values not included in the returned facets
other: 0,
// number of documents which have no value for the field
missing: 0,
// term object ({term: , count: ...})
terms: []
}
});
// ## A Collection/List of Facets
my.FacetList = Backbone.Collection.extend({
model: my.Facet
}); });
// ## Backend registry // ## Backend registry

View File

@@ -168,6 +168,10 @@ my.DataExplorer = Backbone.View.extend({
model: this.model.queryState model: this.model.queryState
}); });
this.el.find('.header').append(queryEditor.el); this.el.find('.header').append(queryEditor.el);
var queryFacetEditor = new my.FacetQueryEditor({
model: this.model
});
this.el.find('.header').append(queryFacetEditor.el);
}, },
setupRouting: function() { setupRouting: function() {
@@ -275,6 +279,61 @@ my.QueryEditor = Backbone.View.extend({
} }
}); });
my.FacetQueryEditor = Backbone.View.extend({
className: 'recline-query-facet-editor',
template: ' \
<div class="dropdown js-add-facet"> \
<a class="btn dropdown-toggle" data-toggle="dropdown" href=".js-add-facet">Add Facet On <i class="caret"></i></a> \
<ul class="dropdown-menu"> \
{{#fields}} \
<li><a href="#{{id}}">{{label}}</a></li> \
{{/fields}} \
</ul> \
</div> \
<div class="facets"> \
{{#facets}} \
<a class="btn js-facet-show-toggle" data-facet="{{id}}"><i class="icon-plus"></i> {{id}} {{label}}</a> \
<ul class="facet-items" data-facet="{{id}}" style="display: none;"> \
{{#terms}} \
<li>{{term}} ({{count}}) <input type="checkbox" class="facet-choice" data-facet="{{label}}" value="{{term}}" /></li> \
{{/terms}} \
</ul> \
{{/facets}} \
</div> \
',
events: {
'click .js-add-facet .dropdown-menu a': 'onAddFacet',
'click .js-facet-show-toggle': 'onFacetShowToggle'
},
initialize: function(model) {
_.bindAll(this, 'render');
this.el = $(this.el);
this.model.facets.bind('all', this.render);
this.model.fields.bind('all', this.render);
this.render();
},
render: function() {
var tmplData = {
facets: this.model.facets.toJSON(),
fields: this.model.fields.toJSON()
};
var templated = $.mustache(this.template, tmplData);
this.el.html(templated);
},
onAddFacet: function(e) {
e.preventDefault();
var fieldId = $(e.target).attr('href').slice(1);
this.model.queryState.addFacet(fieldId);
},
onFacetShowToggle: function(e) {
e.preventDefault();
var $a = $(e.target);
var facetId = $a.attr('data-facet');
var $ul = this.el.find('.facet-items[data-facet="' + facetId + '"]');
$ul.toggle();
}
});
/* ========================================================== */ /* ========================================================== */
// ## Miscellaneous Utilities // ## Miscellaneous Utilities

View File

@@ -7,14 +7,14 @@ var memoryData = {
, name: '1-my-test-dataset' , name: '1-my-test-dataset'
, id: 'test-dataset' , id: 'test-dataset'
}, },
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}], fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
documents: [ documents: [
{id: 0, x: 1, y: 2, z: 3} {id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
, {id: 1, x: 2, y: 4, z: 6} , {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
, {id: 2, x: 3, y: 6, z: 9} , {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
, {id: 3, x: 4, y: 8, z: 12} , {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth'}
, {id: 4, x: 5, y: 10, z: 15} , {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth'}
, {id: 5, x: 6, y: 12, z: 18} , {id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth'}
] ]
}; };
@@ -32,6 +32,8 @@ test('Memory Backend: createDataset', function () {
test('Memory Backend: createDataset 2', function () { test('Memory Backend: createDataset 2', function () {
var dataset = recline.Backend.createDataset(memoryData.documents); var dataset = recline.Backend.createDataset(memoryData.documents);
equal(dataset.fields.length, 6);
deepEqual(['id', 'x', 'y', 'z', 'country', 'label'], dataset.fields.pluck('id'));
dataset.query(); dataset.query();
equal(memoryData.documents.length, dataset.currentDocuments.length); equal(memoryData.documents.length, dataset.currentDocuments.length);
}); });
@@ -71,12 +73,35 @@ test('Memory Backend: query sort', function () {
{'y': {order: 'desc'}} {'y': {order: 'desc'}}
] ]
}; };
dataset.query(queryObj).then(function(docs) { dataset.query(queryObj).then(function() {
var doc0 = dataset.currentDocuments.models[0].toJSON(); var doc0 = dataset.currentDocuments.models[0].toJSON();
equal(doc0.x, 6); equal(doc0.x, 6);
}); });
}); });
test('Memory Backend: facet', function () {
var dataset = makeBackendDataset();
dataset.queryState.addFacet('country');
dataset.query().then(function() {
equal(dataset.facets.length, 1);
var exp = [
{
term: 'UK',
count: 3
},
{
term: 'DE',
count: 2
},
{
term: 'US',
count: 1
}
];
deepEqual(dataset.facets.get('country').toJSON().terms, exp);
});
});
test('Memory Backend: update and delete', function () { test('Memory Backend: update and delete', function () {
var dataset = makeBackendDataset(); var dataset = makeBackendDataset();
// convenience for tests - get the data that should get changed // convenience for tests - get the data that should get changed
@@ -377,7 +402,6 @@ test("GDoc Backend", function() {
); );
var stub = sinon.stub($, 'getJSON', function(options, cb) { var stub = sinon.stub($, 'getJSON', function(options, cb) {
console.log('options are', options, cb);
var partialUrl = 'spreadsheets.google.com'; var partialUrl = 'spreadsheets.google.com';
if (options.indexOf(partialUrl) != -1) { if (options.indexOf(partialUrl) != -1) {
cb(sample_gdocs_spreadsheet_data) cb(sample_gdocs_spreadsheet_data)
@@ -385,12 +409,10 @@ test("GDoc Backend", function() {
}); });
dataset.fetch().then(function(dataset) { dataset.fetch().then(function(dataset) {
console.log('inside dataset:', dataset, dataset.fields, dataset.get('data'));
deepEqual(['column-2', 'column-1'], _.pluck(dataset.fields.toJSON(), 'id')); deepEqual(['column-2', 'column-1'], _.pluck(dataset.fields.toJSON(), 'id'));
//equal(null, dataset.docCount) //equal(null, dataset.docCount)
dataset.query().then(function(docList) { dataset.query().then(function(docList) {
equal(3, docList.length); equal(3, docList.length);
console.log(docList.models[0]);
equal("A", docList.models[0].get('column-1')); equal("A", docList.models[0].get('column-1'));
// needed only if not stubbing // needed only if not stubbing
start(); start();

View File

@@ -1,6 +1,9 @@
(function ($) { (function ($) {
module("Model"); module("Model");
// =================================
// Field
test('Field: basics', function () { test('Field: basics', function () {
var field = new recline.Model.Field({ var field = new recline.Model.Field({
id: 'x' id: 'x'
@@ -35,6 +38,10 @@ test('Field: basics', function () {
equal('XX', out[0].label); equal('XX', out[0].label);
}); });
// =================================
// Dataset
test('Dataset', function () { test('Dataset', function () {
var meta = {id: 'test', title: 'xyz'}; var meta = {id: 'test', title: 'xyz'};
var dataset = new recline.Model.Dataset(meta); var dataset = new recline.Model.Dataset(meta);
@@ -43,4 +50,23 @@ test('Dataset', function () {
equal(out.fields.length, 2); equal(out.fields.length, 2);
}); });
test('Dataset _prepareQuery', function () {
var meta = {id: 'test', title: 'xyz'};
var dataset = new recline.Model.Dataset(meta);
var out = dataset._prepareQuery();
var exp = new recline.Model.Query().toJSON();
deepEqual(out, exp);
});
// =================================
// Query
test('Query', function () {
var query = new recline.Model.Query();
query.addFacet('xyz');
deepEqual({terms: {field: 'xyz'}}, query.get('facets')['xyz']);
});
})(this.jQuery); })(this.jQuery);