Merge branch 'master' into gh-pages
This commit is contained in:
commit
d8c728255c
@ -60,6 +60,10 @@
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.recline-query-facet-editor {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/**********************************************************
|
||||
* Notifications
|
||||
*********************************************************/
|
||||
|
||||
@ -81,19 +81,20 @@ function localDataset() {
|
||||
, name: '1-my-test-dataset'
|
||||
, id: datasetId
|
||||
},
|
||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'label'}],
|
||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
|
||||
documents: [
|
||||
{id: 0, x: 1, y: 2, z: 3, label: 'first'}
|
||||
, {id: 1, x: 2, y: 4, z: 6, label: 'second'}
|
||||
, {id: 2, x: 3, y: 6, z: 9, label: 'third'}
|
||||
, {id: 3, x: 4, y: 8, z: 12, label: 'fourth'}
|
||||
, {id: 4, x: 5, y: 10, z: 15, label: 'fifth'}
|
||||
, {id: 5, x: 6, y: 12, z: 18, label: 'sixth'}
|
||||
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
|
||||
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
|
||||
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
|
||||
, {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth'}
|
||||
, {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth'}
|
||||
, {id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth'}
|
||||
]
|
||||
};
|
||||
var backend = new recline.Backend.Memory();
|
||||
backend.addDataset(inData);
|
||||
var dataset = new recline.Model.Dataset({id: datasetId}, backend);
|
||||
dataset.queryState.addFacet('country');
|
||||
return dataset;
|
||||
}
|
||||
|
||||
|
||||
67
index.html
67
index.html
@ -9,9 +9,9 @@
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![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="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">
|
||||
html, body {
|
||||
@ -65,7 +65,6 @@
|
||||
<ul class="nav">
|
||||
<li><a href="demo/">Demo</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>
|
||||
</ul>
|
||||
<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
|
||||
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
|
||||
distinction inherent in Backbone.</p>
|
||||
|
||||
@ -154,9 +153,13 @@ Backbone.history.start();
|
||||
<p><strong>Backends</strong> connect Dataset and Documents to data
|
||||
from a specific 'Backend' data source. They provide methods for loading and
|
||||
saving Datasets and individuals Documents as well as for bulk loading via a
|
||||
query API and doing bulk transforms on the backend. More <a
|
||||
href="#doc-backends">info on backends can be found below</a>.</p>
|
||||
|
||||
query API and doing bulk transforms on the backend.</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>
|
||||
<ul>
|
||||
<li>DataExplorer: the parent view which manages the overall app and sets up sub views.</li>
|
||||
@ -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>
|
||||
</ul>
|
||||
|
||||
<h3 id="doc-backends">Backends</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>
|
||||
<h3 id="docs-source">Source Docs (via Docco)</h3>
|
||||
<ul>
|
||||
<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-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/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/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>
|
||||
</ul>
|
||||
|
||||
@ -258,7 +219,7 @@ like).</p>
|
||||
<ul class="nav nav-list">
|
||||
<li><a href="#docs-using">Using it</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>
|
||||
</div><!--/.well -->
|
||||
</div><!--/span-->
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
//
|
||||
// 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.Backend = this.recline.Backend || {};
|
||||
|
||||
@ -14,29 +14,110 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
return model.backend.sync(method, model, options);
|
||||
}
|
||||
|
||||
// ## wrapInTimeout
|
||||
//
|
||||
// Crude way to catch backend errors
|
||||
// Many of backends use JSONP and so will not get error messages and this is
|
||||
// a crude way to catch those errors.
|
||||
my.wrapInTimeout = function(ourFunction) {
|
||||
var dfd = $.Deferred();
|
||||
var timeout = 5000;
|
||||
var timer = setTimeout(function() {
|
||||
dfd.reject({
|
||||
message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
|
||||
// ## recline.Backend.Base
|
||||
//
|
||||
// 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 };
|
||||
});
|
||||
}, timeout);
|
||||
ourFunction.done(function(arguments) {
|
||||
clearTimeout(timer);
|
||||
dfd.resolve(arguments);
|
||||
})
|
||||
.fail(function(arguments) {
|
||||
clearTimeout(timer);
|
||||
dfd.reject(arguments);
|
||||
})
|
||||
;
|
||||
return dfd.promise();
|
||||
}
|
||||
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
|
||||
// a crude way to catch those errors.
|
||||
_wrapInTimeout: function(ourFunction) {
|
||||
var dfd = $.Deferred();
|
||||
var timeout = 5000;
|
||||
var timer = setTimeout(function() {
|
||||
dfd.reject({
|
||||
message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
|
||||
});
|
||||
}, timeout);
|
||||
ourFunction.done(function(arguments) {
|
||||
clearTimeout(timer);
|
||||
dfd.resolve(arguments);
|
||||
})
|
||||
.fail(function(arguments) {
|
||||
clearTimeout(timer);
|
||||
dfd.reject(arguments);
|
||||
})
|
||||
;
|
||||
return dfd.promise();
|
||||
}
|
||||
});
|
||||
|
||||
}(jQuery, this.recline.Backend));
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// * format: (optional) csv | xls (defaults to csv if not specified)
|
||||
//
|
||||
// Note that this is a **read-only** backend.
|
||||
my.DataProxy = Backbone.Model.extend({
|
||||
my.DataProxy = my.Base.extend({
|
||||
defaults: {
|
||||
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
|
||||
},
|
||||
@ -35,6 +35,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
}
|
||||
},
|
||||
query: function(dataset, queryObj) {
|
||||
var self = this;
|
||||
var base = this.get('dataproxy_url');
|
||||
var data = {
|
||||
url: dataset.get('url')
|
||||
@ -47,7 +48,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
, dataType: 'jsonp'
|
||||
});
|
||||
var dfd = $.Deferred();
|
||||
my.wrapInTimeout(jqxhr).done(function(results) {
|
||||
this._wrapInTimeout(jqxhr).done(function(results) {
|
||||
if (results.error) {
|
||||
dfd.reject(results.error);
|
||||
}
|
||||
@ -62,7 +63,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
});
|
||||
return tmp;
|
||||
});
|
||||
dfd.resolve(_out);
|
||||
dfd.resolve(self._docsToQueryResult(_out));
|
||||
})
|
||||
.fail(function(arguments) {
|
||||
dfd.reject(arguments);
|
||||
|
||||
@ -19,7 +19,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// localhost:9200 with index twitter and type tweet it would be
|
||||
//
|
||||
// <pre>http://localhost:9200/twitter/tweet</pre>
|
||||
my.ElasticSearch = Backbone.Model.extend({
|
||||
my.ElasticSearch = my.Base.extend({
|
||||
_getESUrl: function(dataset) {
|
||||
var out = dataset.get('elasticsearch_url');
|
||||
if (out) return out;
|
||||
@ -39,7 +39,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
dataType: 'jsonp'
|
||||
});
|
||||
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
|
||||
var key = _.keys(schema)[0];
|
||||
var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
|
||||
@ -93,13 +93,15 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
var dfd = $.Deferred();
|
||||
// TODO: fail case
|
||||
jqxhr.done(function(results) {
|
||||
model.docCount = results.hits.total;
|
||||
var docs = _.map(results.hits.hits, function(result) {
|
||||
var _out = result._source;
|
||||
_out.id = result._id;
|
||||
return _out;
|
||||
});
|
||||
dfd.resolve(docs);
|
||||
_.each(results.hits.hits, function(hit) {
|
||||
if (!'id' in hit._source && hit._id) {
|
||||
hit._source.id = hit._id;
|
||||
}
|
||||
})
|
||||
if (results.facets) {
|
||||
results.hits.facets = results.facets;
|
||||
}
|
||||
dfd.resolve(results.hits);
|
||||
});
|
||||
return dfd.promise();
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// 'gdocs'
|
||||
// );
|
||||
// </pre>
|
||||
my.GDoc = Backbone.Model.extend({
|
||||
my.GDoc = my.Base.extend({
|
||||
getUrl: function(dataset) {
|
||||
var url = dataset.get('url');
|
||||
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]; })
|
||||
return obj;
|
||||
});
|
||||
dfd.resolve(objs);
|
||||
dfd.resolve(this._docsToQueryResult(objs));
|
||||
return dfd;
|
||||
},
|
||||
gdocsToJavascript: function(gdocsSpreadsheet) {
|
||||
|
||||
@ -29,8 +29,8 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
datasetInfo.fields = fields;
|
||||
} else {
|
||||
if (data) {
|
||||
datasetInfo.fields = _.map(data[0], function(cell) {
|
||||
return {id: cell};
|
||||
datasetInfo.fields = _.map(data[0], function(value, key) {
|
||||
return {id: key};
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -69,7 +69,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// dataset.fetch();
|
||||
// etc ...
|
||||
// </pre>
|
||||
my.Memory = Backbone.Model.extend({
|
||||
my.Memory = my.Base.extend({
|
||||
initialize: function() {
|
||||
this.datasets = {};
|
||||
},
|
||||
@ -115,9 +115,10 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
}
|
||||
},
|
||||
query: function(model, queryObj) {
|
||||
var dfd = $.Deferred();
|
||||
var out = {};
|
||||
var numRows = queryObj.size;
|
||||
var start = queryObj.from;
|
||||
var dfd = $.Deferred();
|
||||
results = this.datasets[model.id].documents;
|
||||
// not complete sorting!
|
||||
_.each(queryObj.sort, function(sortObj) {
|
||||
@ -127,9 +128,48 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
|
||||
});
|
||||
});
|
||||
var results = results.slice(start, start+numRows);
|
||||
dfd.resolve(results);
|
||||
out.facets = this._computeFacets(results, queryObj);
|
||||
var total = results.length;
|
||||
resultsObj = this._docsToQueryResult(results.slice(start, start+numRows));
|
||||
_.extend(out, resultsObj);
|
||||
out.total = total;
|
||||
dfd.resolve(out);
|
||||
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();
|
||||
|
||||
66
src/model.js
66
src/model.js
@ -23,9 +23,11 @@ my.Dataset = Backbone.Model.extend({
|
||||
}
|
||||
this.fields = new my.FieldList();
|
||||
this.currentDocuments = new my.DocumentList();
|
||||
this.facets = new my.FacetList();
|
||||
this.docCount = null;
|
||||
this.queryState = new my.Query();
|
||||
this.queryState.bind('change', this.query);
|
||||
this.queryState.bind('facet:add', this.query);
|
||||
},
|
||||
|
||||
// ### query
|
||||
@ -38,18 +40,26 @@ my.Dataset = Backbone.Model.extend({
|
||||
// Resulting DocumentList are used to reset this.currentDocuments and are
|
||||
// also returned.
|
||||
query: function(queryObj) {
|
||||
this.trigger('query:start');
|
||||
var self = this;
|
||||
this.queryState.set(queryObj);
|
||||
this.trigger('query:start');
|
||||
var actualQuery = self._prepareQuery(queryObj);
|
||||
var dfd = $.Deferred();
|
||||
this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
|
||||
var docs = _.map(rows, function(row) {
|
||||
var _doc = new my.Document(row);
|
||||
this.backend.query(this, actualQuery).done(function(queryResult) {
|
||||
self.docCount = queryResult.total;
|
||||
var docs = _.map(queryResult.hits, function(hit) {
|
||||
var _doc = new my.Document(hit._source);
|
||||
_doc.backend = self.backend;
|
||||
_doc.dataset = self;
|
||||
return _doc;
|
||||
});
|
||||
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');
|
||||
dfd.resolve(self.currentDocuments);
|
||||
})
|
||||
@ -60,6 +70,14 @@ my.Dataset = Backbone.Model.extend({
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
_prepareQuery: function(newQueryObj) {
|
||||
if (newQueryObj) {
|
||||
this.queryState.set(newQueryObj);
|
||||
}
|
||||
var out = this.queryState.toJSON();
|
||||
return out;
|
||||
},
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var data = this.toJSON();
|
||||
data.docCount = this.docCount;
|
||||
@ -116,9 +134,47 @@ my.Query = Backbone.Model.extend({
|
||||
defaults: {
|
||||
size: 100
|
||||
, 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
|
||||
//
|
||||
// Backends will register themselves by id into this registry
|
||||
|
||||
59
src/view.js
59
src/view.js
@ -168,6 +168,10 @@ my.DataExplorer = Backbone.View.extend({
|
||||
model: this.model.queryState
|
||||
});
|
||||
this.el.find('.header').append(queryEditor.el);
|
||||
var queryFacetEditor = new my.FacetQueryEditor({
|
||||
model: this.model
|
||||
});
|
||||
this.el.find('.header').append(queryFacetEditor.el);
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
@ -7,14 +7,14 @@ var memoryData = {
|
||||
, name: '1-my-test-dataset'
|
||||
, id: 'test-dataset'
|
||||
},
|
||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
|
||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
|
||||
documents: [
|
||||
{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}
|
||||
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
|
||||
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
|
||||
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
|
||||
, {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth'}
|
||||
, {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth'}
|
||||
, {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 () {
|
||||
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();
|
||||
equal(memoryData.documents.length, dataset.currentDocuments.length);
|
||||
});
|
||||
@ -71,11 +73,34 @@ test('Memory Backend: query sort', function () {
|
||||
{'y': {order: 'desc'}}
|
||||
]
|
||||
};
|
||||
dataset.query(queryObj).then(function(docs) {
|
||||
dataset.query(queryObj).then(function() {
|
||||
var doc0 = dataset.currentDocuments.models[0].toJSON();
|
||||
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 () {
|
||||
var dataset = makeBackendDataset();
|
||||
@ -377,7 +402,6 @@ test("GDoc Backend", function() {
|
||||
);
|
||||
|
||||
var stub = sinon.stub($, 'getJSON', function(options, cb) {
|
||||
console.log('options are', options, cb);
|
||||
var partialUrl = 'spreadsheets.google.com';
|
||||
if (options.indexOf(partialUrl) != -1) {
|
||||
cb(sample_gdocs_spreadsheet_data)
|
||||
@ -385,12 +409,10 @@ test("GDoc Backend", function() {
|
||||
});
|
||||
|
||||
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'));
|
||||
//equal(null, dataset.docCount)
|
||||
dataset.query().then(function(docList) {
|
||||
equal(3, docList.length);
|
||||
console.log(docList.models[0]);
|
||||
equal("A", docList.models[0].get('column-1'));
|
||||
// needed only if not stubbing
|
||||
start();
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
(function ($) {
|
||||
module("Model");
|
||||
|
||||
// =================================
|
||||
// Field
|
||||
|
||||
test('Field: basics', function () {
|
||||
var field = new recline.Model.Field({
|
||||
id: 'x'
|
||||
@ -35,6 +38,10 @@ test('Field: basics', function () {
|
||||
equal('XX', out[0].label);
|
||||
});
|
||||
|
||||
|
||||
// =================================
|
||||
// Dataset
|
||||
|
||||
test('Dataset', function () {
|
||||
var meta = {id: 'test', title: 'xyz'};
|
||||
var dataset = new recline.Model.Dataset(meta);
|
||||
@ -43,4 +50,23 @@ test('Dataset', function () {
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user