// # Recline Backbone Models this.recline = this.recline || {}; this.recline.Model = this.recline.Model || {}; (function($, my) { // ## A Dataset model // // 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 // // Sets up instance properties (see above) 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); }, // ### 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; } }); // ## A Document (aka Row) // // A single entry or row in the dataset my.Document = Backbone.Model.extend({ __type__: 'Document', initialize: function() { _.bindAll(this, 'getFieldValue'); }, // ### 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; } }); // ## A Backbone collection of Documents my.DocumentList = Backbone.Collection.extend({ __type__: 'DocumentList', model: my.Document }); // ## A Field (aka Column) on a Dataset // // Following (Backbone) attributes as standard: // // * id: a unique identifer for this field- usually this should match the key in the documents hash // * label: (optional: defaults to id) the visible label used for this field // * type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on // * format: (optional) used to indicate how the data should be formatted. For example: // * type=date, format=yyyy-mm-dd // * type=float, format=percentage // * type=float, format='###,###.##' // * is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below). // // 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 - define default values defaults: { label: null, type: 'string', format: null, is_derived: false }, // ### initialize // // @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 // // 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](http://www.elasticsearch.org/guide/reference/api/search/). // // **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: // // * size (=limit): number of results to return // * from (=offset): offset into result set - http://www.elasticsearch.org/guide/reference/api/search/from-size.html // * sort: sort order - // * query: Query in ES Query DSL // * filter: See filters and Filtered Query // * fields: set of fields to return - http://www.elasticsearch.org/guide/reference/api/search/fields.html // * facets: specification of facets - see http://www.elasticsearch.org/guide/reference/api/search/facets/ // // Additions: // // * q: either straight text or a hash will map directly onto a [query_string // query](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html) // 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: {}, // // , filter: {} filters: [] }; }, // #### addTermFilter // // Set (update or add) a terms filter to filters // // See 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 // // Remove a filter from filters at index filterIndex removeFilter: function(filterIndex) { var filters = this.get('filters'); filters.splice(filterIndex, 1); this.set({filters: filters}); this.trigger('change'); }, // ### addFacet // // Add a Facet to this query // // See 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); } }); // ## A Facet (Result) // // 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: // // // 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: [] }; } }); // ## A Collection/List of Facets my.FacetList = Backbone.Collection.extend({ model: my.Facet }); // ## Backend registry // // Backends will register themselves by id into this registry my.backends = {}; }(jQuery, this.recline.Model));