Merge branch 'master' into gh-pages

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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