this.recline = this.recline || {};
this.recline.Model = this.recline.Model || {};
(function($, my) {A model has the following (non-Backbone) attributes:
@property {FieldList} fields: (aka columns) is a FieldList listing all the
fields on this Dataset (this can be set explicitly, or, will be set by
Dataset.fetch() or Dataset.query()
@property {DocumentList} currentDocuments: a DocumentList containing the
Documents we have currently loaded for viewing (updated by calling query
method)
@property {number} docCount: total number of documents in this dataset
@property {Backend} backend: the Backend (instance) for this Dataset
@property {Query} queryState: Query object which stores current
queryState. queryState may be edited by other components (e.g. a query
editor view) changes will trigger a Dataset query.
@property {FacetList} facets: FacetList object containing all current Facets.
my.Dataset = Backbone.Model.extend({
__type__: 'Dataset', initialize: function(model, backend) {
_.bindAll(this, 'query');
this.backend = backend;
if (backend && backend.constructor == String) {
this.backend = my.backends[backend];
}
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);
},AJAX method with promise API to get documents from the backend.
It will query based on current query state (given by this.queryState) updated by queryObj (if provided).
Resulting DocumentList are used to reset this.currentDocuments and are also returned.
query: function(queryObj) {
var self = this;
this.trigger('query:start');
var actualQuery = self._prepareQuery(queryObj);
var dfd = $.Deferred();
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);
})
.fail(function(arguments) {
self.trigger('query:fail', arguments);
dfd.reject(arguments);
});
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;
data.fields = this.fields.toJSON();
return data;
}
});my.Document = Backbone.Model.extend({
__type__: 'Document',
initialize: function() {
_.bindAll(this, 'getFieldValue');
},For the provided Field get the corresponding rendered computed data value for this document.
getFieldValue: function(field) {
var val = this.get(field.id);
if (field.deriver) {
val = field.deriver(val, field, this);
}
if (field.renderer) {
val = field.renderer(val, field, this);
}
return val;
}
});my.DocumentList = Backbone.Collection.extend({
__type__: 'DocumentList',
model: my.Document
});Following (Backbone) attributes as standard:
Following additional instance properties:
@property {Function} renderer: a function to render the data for this field. Signature: function(value, field, doc) where value is the value of this cell, field is corresponding field object and document is the document object. Note that implementing functions can ignore arguments (e.g. function(value) would be a valid formatter function).
@property {Function} deriver: a function to derive/compute the value of data in this field as a function of this field's value (if any) and the current document, its signature and behaviour is the same as for renderer. Use of this function allows you to define an entirely new value for data in this field. This provides support for a) 'derived/computed' fields: i.e. fields whose data are functions of the data in other fields b) transforming the value of this field prior to rendering.
my.Field = Backbone.Model.extend({ defaults: {
label: null,
type: 'string',
format: null,
is_derived: false
},@param {Object} data: standard Backbone model attributes
@param {Object} options: renderer and/or deriver functions.
initialize: function(data, options) {if a hash not passed in the first argument throw error
if ('0' in data) {
throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
}
if (this.attributes.label === null) {
this.set({label: this.id});
}
if (options) {
this.renderer = options.renderer;
this.deriver = options.deriver;
}
if (!this.renderer) {
this.renderer = this.defaultRenderers[this.get('type')];
}
},
defaultRenderers: {
object: function(val, field, doc) {
return JSON.stringify(val);
},
'float': function(val, field, doc) {
var format = field.get('format');
if (format === 'percentage') {
return val + '%';
}
}
}
});
my.FieldList = Backbone.Collection.extend({
model: my.Field
});Query instances encapsulate a query to the backend (see query method on backend). Useful both for creating queries and for storing and manipulating query state - e.g. from a query editor).
Query Structure and format
Query structure should follow that of ElasticSearch query language.
NB: It is up to specific backends how to implement and support this query structure. Different backends might choose to implement things differently or not support certain features. Please check your backend for details.
Query object has the following key attributes:
Additions:
q: either straight text or a hash will map directly onto a query_string query in backend
Of course this can be re-interpreted by different backends. E.g. some may just pass this straight through e.g. for an SQL backend this could be the full SQL query
filters: dict of ElasticSearch filters. These will be and-ed together for execution.
Examples
{
q: 'quick brown fox',
filters: [
{ term: { 'owner': 'jones' } }
]
}
my.Query = Backbone.Model.extend({
defaults: function() {
return {
size: 100,
from: 0,
facets: {}, filters: []
};
},Set (update or add) a terms filter to filters
See http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html
addTermFilter: function(fieldId, value) {
var filters = this.get('filters');
var filter = { term: {} };
filter.term[fieldId] = value;
filters.push(filter);
this.set({filters: filters});change does not seem to be triggered automatically
if (value) {
this.trigger('change');
} else {adding a new blank filter and do not want to trigger a new query
this.trigger('change:filters:new-blank');
}
}, removeFilter: function(filterIndex) {
var filters = this.get('filters');
filters.splice(filterIndex, 1);
this.set({filters: filters});
this.trigger('change');
},Add a Facet to this query
See http://www.elasticsearch.org/guide/reference/api/search/facets/
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);
},
addHistogramFacet: function(fieldId) {
var facets = this.get('facets');
facets[fieldId] = {
date_histogram: {
field: fieldId,
interval: 'day'
}
};
this.set({facets: facets}, {silent: true});
this.trigger('facet:add', this);
}
});Object to store Facet information, that is summary information (e.g. values and counts) about a field obtained by some faceting method on the backend.
Structure of a facet follows that of Facet results in ElasticSearch, see: http://www.elasticsearch.org/guide/reference/api/search/facets/
Specifically the object structure of a facet looks like (there is one addition compared to ElasticSearch: the "id" field which corresponds to the key used to specify this facet in the facet query):
{
"id": "id-of-facet",
// type of this facet (terms, range, histogram etc)
"_type" : "terms",
// total number of tokens in the facet
"total": 5,
// @property {number} number of documents which have no value for the field
"missing" : 0,
// number of facet values not included in the returned facets
"other": 0,
// term object ({term: , count: ...})
"terms" : [ {
"term" : "foo",
"count" : 2
}, {
"term" : "bar",
"count" : 2
}, {
"term" : "baz",
"count" : 1
}
]
}
my.Facet = Backbone.Model.extend({
defaults: function() {
return {
_type: 'terms',
total: 0,
other: 0,
missing: 0,
terms: []
};
}
});my.FacetList = Backbone.Collection.extend({
model: my.Facet
});my.backends = {};
}(jQuery, this.recline.Model));