• Jump To … +
    backend.dataproxy.js backend.memory.js ecma-fixes.js model.js view.flot.js view.graph.js view.grid.js view.map.js view.multiview.js view.slickgrid.js view.timeline.js widget.facetviewer.js widget.fields.js widget.filtereditor.js widget.pager.js widget.queryeditor.js widget.valuefilter.js
  • ¶

    Recline Backbone Models

    this.recline = this.recline || {};
    this.recline.Model = this.recline.Model || {};
    
    (function(my) {
      "use strict";
  • ¶

    use either jQuery or Underscore Deferred depending on what is available

    var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;
  • ¶

    Dataset

    my.Dataset = Backbone.Model.extend({
      constructor: function Dataset() {
        Backbone.Model.prototype.constructor.apply(this, arguments);
      },
  • ¶

    initialize

      initialize: function() {
        var self = this;
        _.bindAll(this, 'query');
        this.backend = null;
        if (this.get('backend')) {
          this.backend = this._backendFromString(this.get('backend'));
        } else { // try to guess backend ...
          if (this.get('records')) {
            this.backend = recline.Backend.Memory;
          }
        }
        this.fields = new my.FieldList();
        this.records = new my.RecordList();
        this._changes = {
          deletes: [],
          updates: [],
          creates: []
        };
        this.facets = new my.FacetList();
        this.recordCount = null;
        this.queryState = new my.Query();
        this.queryState.bind('change facet:add', function () {
          self.query(); // We want to call query() without any arguments.
        });
  • ¶

    store is what we query and save against store will either be the backend or be a memory store if Backend fetch tells us to use memory store

        this._store = this.backend;
  • ¶

    if backend has a handleQueryResultFunction, use that

        this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ? 
          this.backend.handleQueryResult : this._handleQueryResult;
        if (this.backend == recline.Backend.Memory) {
          this.fetch();
        }
      },
    
      sync: function(method, model, options) {
        return this.backend.sync(method, model, options);
      },
  • ¶

    fetch

    Retrieve dataset and (some) records from the backend.

      fetch: function() {
        var self = this;
        var dfd = new Deferred();
    
        if (this.backend !== recline.Backend.Memory) {
          this.backend.fetch(this.toJSON())
            .done(handleResults)
            .fail(function(args) {
              dfd.reject(args);
            });
        } else {
  • ¶

    special case where we have been given data directly

          handleResults({
            records: this.get('records'),
            fields: this.get('fields'),
            useMemoryStore: true
          });
        }
    
        function handleResults(results) {
  • ¶

    if explicitly given the fields (e.g. var dataset = new Dataset({fields: fields, …}) use that field info over anything we get back by parsing the data (results.fields)

          var fields = self.get('fields') || results.fields;
    
          var out = self._normalizeRecordsAndFields(results.records, fields);
          if (results.useMemoryStore) {
            self._store = new recline.Backend.Memory.Store(out.records, out.fields);
          }
    
          self.set(results.metadata);
          self.fields.reset(out.fields);
          self.query()
            .done(function() {
              dfd.resolve(self);
            })
            .fail(function(args) {
              dfd.reject(args);
            });
        }
    
        return dfd.promise();
      },
  • ¶

    _normalizeRecordsAndFields

    Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects

    e.g. fields = [‘a’, ‘b’, ‘c’] and records = [ [1,2,3] ] => fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]

      _normalizeRecordsAndFields: function(records, fields) {
  • ¶

    if no fields get them from records

        if (!fields && records && records.length > 0) {
  • ¶

    records is array then fields is first row of records …

          if (records[0] instanceof Array) {
            fields = records[0];
            records = records.slice(1);
          } else {
            fields = _.map(_.keys(records[0]), function(key) {
              return {id: key};
            });
          }
        }
  • ¶

    fields is an array of strings (i.e. list of field headings/ids)

        if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) {
  • ¶

    Rename duplicate fieldIds as each field name needs to be unique.

          var seen = {};
          fields = _.map(fields, function(field, index) {
            if (field === null) {
              field = '';
            } else {
              field = field.toString();
            }
  • ¶

    cannot use trim as not supported by IE7

            var fieldId = field.replace(/^\s+|\s+$/g, '');
            if (fieldId === '') {
              fieldId = '_noname_';
              field = fieldId;
            }
            while (fieldId in seen) {
              seen[field] += 1;
              fieldId = field + seen[field];
            }
            if (!(field in seen)) {
              seen[field] = 0;
            }
  • ¶

    TODO: decide whether to keep original name as label … return { id: fieldId, label: field || fieldId }

            return { id: fieldId };
          });
        }
  • ¶

    records is provided as arrays so need to zip together with fields NB: this requires you to have fields to match arrays

        if (records && records.length > 0 && records[0] instanceof Array) {
          records = _.map(records, function(doc) {
            var tmp = {};
            _.each(fields, function(field, idx) {
              tmp[field.id] = doc[idx];
            });
            return tmp;
          });
        }
        return {
          fields: fields,
          records: records
        };
      },
    
      save: function() {
        var self = this;
  • ¶

    TODO: need to reset the changes …

        return this._store.save(this._changes, this.toJSON());
      },
  • ¶

    query

    AJAX method with promise API to get records from the backend.

    It will query based on current query state (given by this.queryState) updated by queryObj (if provided).

    Resulting RecordList are used to reset this.records and are also returned.

      query: function(queryObj) {
        var self = this;
        var dfd = new Deferred();
        this.trigger('query:start');
    
        if (queryObj) {
          var attributes = queryObj;
          if (queryObj instanceof my.Query) {
            attributes = queryObj.toJSON();
          }
          this.queryState.set(attributes, {silent: true});
        }
        var actualQuery = this.queryState.toJSON();
    
        this._store.query(actualQuery, this.toJSON())
          .done(function(queryResult) {
            self._handleResult(queryResult);
            self.trigger('query:done');
            dfd.resolve(self.records);
          })
          .fail(function(args) {
            self.trigger('query:fail', args);
            dfd.reject(args);
          });
        return dfd.promise();
      },
    
      _handleQueryResult: function(queryResult) {
        var self = this;
        self.recordCount = queryResult.total;
        var docs = _.map(queryResult.hits, function(hit) {
          var _doc = new my.Record(hit);
          _doc.fields = self.fields;
          _doc.bind('change', function(doc) {
            self._changes.updates.push(doc.toJSON());
          });
          _doc.bind('destroy', function(doc) {
            self._changes.deletes.push(doc.toJSON());
          });
          return _doc;
        });
        self.records.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);
        }
      },
    
      toTemplateJSON: function() {
        var data = this.toJSON();
        data.recordCount = this.recordCount;
        data.fields = this.fields.toJSON();
        return data;
      },
  • ¶

    getFieldsSummary

    Get a summary for each field in the form of a Facet.

    @return null as this is async function. Provides deferred/promise interface.

      getFieldsSummary: function() {
        var self = this;
        var query = new my.Query();
        query.set({size: 0});
        this.fields.each(function(field) {
          query.addFacet(field.id);
        });
        var dfd = new Deferred();
        this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
          if (queryResult.facets) {
            _.each(queryResult.facets, function(facetResult, facetId) {
              facetResult.id = facetId;
              var facet = new my.Facet(facetResult);
  • ¶

    TODO: probably want replace rather than reset (i.e. just replace the facet with this id)

              self.fields.get(facetId).facets.reset(facet);
            });
          }
          dfd.resolve(queryResult);
        });
        return dfd.promise();
      },
  • ¶

    Deprecated (as of v0.5) - use record.summary()

      recordSummary: function(record) {
        return record.summary();
      },
  • ¶

    _backendFromString(backendString)

    Look up a backend module from a backend string (look in recline.Backend)

      _backendFromString: function(backendString) {
        var backend = null;
        if (recline && recline.Backend) {
          _.each(_.keys(recline.Backend), function(name) {
            if (name.toLowerCase() === backendString.toLowerCase()) {
              backend = recline.Backend[name];
            }
          });
        }
        return backend;
      }
    });
  • ¶

    A Record

    A single record (or row) in the dataset

    my.Record = Backbone.Model.extend({
      constructor: function Record() {
        Backbone.Model.prototype.constructor.apply(this, arguments);
      },
  • ¶

    initialize

    Create a Record

    You usually will not do this directly but will have records created by Dataset e.g. in query method

    Certain methods require presence of a fields attribute (identical to that on Dataset)

      initialize: function() {
        _.bindAll(this, 'getFieldValue');
      },
  • ¶

    getFieldValue

    For the provided Field get the corresponding rendered computed data value for this record.

    NB: if field is undefined a default ‘’ value will be returned

      getFieldValue: function(field) {
        var val = this.getFieldValueUnrendered(field);
        if (field && !_.isUndefined(field.renderer)) {
          val = field.renderer(val, field, this.toJSON());
        }
        return val;
      },
  • ¶

    getFieldValueUnrendered

    For the provided Field get the corresponding computed data value for this record.

    NB: if field is undefined a default ‘’ value will be returned

      getFieldValueUnrendered: function(field) {
        if (!field) {
          return '';
        }
        var val = this.get(field.id);
        if (field.deriver) {
          val = field.deriver(val, field, this);
        }
        return val;
      },
  • ¶

    summary

    Get a simple html summary of this record in form of key/value list

      summary: function(record) {
        var self = this;
        var html = '<div class="recline-record-summary">';
        this.fields.each(function(field) { 
          if (field.id != 'id') {
            html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + self.getFieldValue(field) + '</div>';
          }
        });
        html += '</div>';
        return html;
      },
  • ¶

    Override Backbone save, fetch and destroy so they do nothing Instead, Dataset object that created this Record should take care of handling these changes (discovery will occur via event notifications) WARNING: these will not persist unless you call save on Dataset

      fetch: function() {},
      save: function() {},
      destroy: function() { this.trigger('destroy', this); }
    });
  • ¶

    A Backbone collection of Records

    my.RecordList = Backbone.Collection.extend({
      constructor: function RecordList() {
        Backbone.Collection.prototype.constructor.apply(this, arguments);
      },
      model: my.Record
    });
  • ¶

    A Field (aka Column) on a Dataset

    my.Field = Backbone.Model.extend({
      constructor: function Field() {
        Backbone.Model.prototype.constructor.apply(this, arguments);
      },
  • ¶

    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 (this.attributes.type.toLowerCase() in this._typeMap) {
          this.attributes.type = this._typeMap[this.attributes.type.toLowerCase()];
        }
        if (options) {
          this.renderer = options.renderer;
          this.deriver = options.deriver;
        }
        if (!this.renderer) {
          this.renderer = this.defaultRenderers[this.get('type')];
        }
        this.facets = new my.FacetList();
      },
      _typeMap: {
        'text': 'string',
        'double': 'number',
        'float': 'number',
        'numeric': 'number',
        'int': 'integer',
        'datetime': 'date-time',
        'bool': 'boolean',
        'timestamp': 'date-time',
        'json': 'object'
      },
      defaultRenderers: {
        object: function(val, field, doc) {
          return JSON.stringify(val);
        },
        geo_point: function(val, field, doc) {
          return JSON.stringify(val);
        },
        'number': function(val, field, doc) {
          var format = field.get('format'); 
          if (format === 'percentage') {
            return val + '%';
          }
          return val;
        },
        'string': function(val, field, doc) {
          var format = field.get('format');
          if (format === 'markdown') {
            if (typeof Showdown !== 'undefined') {
              var showdown = new Showdown.converter();
              out = showdown.makeHtml(val);
              return out;
            } else {
              return val;
            }
          } else if (format == 'plain') {
            return val;
          } else {
  • ¶

    as this is the default and default type is string may get things here that are not actually strings

            if (val && typeof val === 'string') {
              val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
            }
            return val;
          }
        }
      }
    });
    
    my.FieldList = Backbone.Collection.extend({
      constructor: function FieldList() {
        Backbone.Collection.prototype.constructor.apply(this, arguments);
      },
      model: my.Field
    });
  • ¶

    Query

    my.Query = Backbone.Model.extend({
      constructor: function Query() {
        Backbone.Model.prototype.constructor.apply(this, arguments);
      },
      defaults: function() {
        return {
          size: 100,
          from: 0,
          q: '',
          facets: {},
          filters: []
        };
      },
      _filterTemplates: {
        term: {
          type: 'term',
  • ¶

    TODO do we need this attribute here?

          field: '',
          term: ''
        },
        range: {
          type: 'range',
          from: '',
          to: ''
        },
        geo_distance: {
          type: 'geo_distance',
          distance: 10,
          unit: 'km',
          point: {
            lon: 0,
            lat: 0
          }
        }
      },
  • ¶

    addFilter(filter)

    Add a new filter specified by the filter hash and append to the list of filters

    @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates

      addFilter: function(filter) {
  • ¶

    crude deep copy

        var ourfilter = JSON.parse(JSON.stringify(filter));
  • ¶

    not fully specified so use template and over-write

        if (_.keys(filter).length <= 3) {
          ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]);
        }
        var filters = this.get('filters');
        filters.push(ourfilter);
        this.trigger('change:filters:new-blank');
      },
      replaceFilter: function(filter) {
  • ¶

    delete filter on the same field, then add

        var filters = this.get('filters');
        var idx = -1;
        _.each(this.get('filters'), function(f, key, list) {
          if (filter.field == f.field) {
            idx = key;
          }
        });
  • ¶

    trigger just one event (change:filters:new-blank) instead of one for remove and one for add

        if (idx >= 0) {
          filters.splice(idx, 1);
          this.set({filters: filters});
        }
        this.addFilter(filter);
      },
      updateFilter: function(index, value) {
      },
  • ¶

    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 http://www.elasticsearch.org/guide/reference/api/search/facets/

      addFacet: function(fieldId, size, silent) {
        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 }
        };
        if (!_.isUndefined(size)) {
          facets[fieldId].terms.size = size;
        }
        this.set({facets: facets}, {silent: true});
        if (!silent) {
          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);
      },
      removeFacet: 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;
        }
        delete facets[fieldId];
        this.set({facets: facets}, {silent: true});
        this.trigger('facet:remove', this);
      },
      clearFacets: function() {
        var facets = this.get('facets');
        _.each(_.keys(facets), function(fieldId) {
          delete facets[fieldId];
        });
        this.trigger('facet:remove', this);
      },
  • ¶

    trigger a facet add; use this to trigger a single event after adding multiple facets

      refreshFacets: function() {
        this.trigger('facet:add', this);
      }
    
    });
  • ¶

    A Facet (Result)

    my.Facet = Backbone.Model.extend({
      constructor: function Facet() {
        Backbone.Model.prototype.constructor.apply(this, arguments);
      },
      defaults: function() {
        return {
          _type: 'terms',
          total: 0,
          other: 0,
          missing: 0,
          terms: []
        };
      }
    });
  • ¶

    A Collection/List of Facets

    my.FacetList = Backbone.Collection.extend({
      constructor: function FacetList() {
        Backbone.Collection.prototype.constructor.apply(this, arguments);
      },
      model: my.Facet
    });
  • ¶

    Object State

    Convenience Backbone model for storing (configuration) state of objects like Views.

    my.ObjectState = Backbone.Model.extend({
    });
  • ¶

    Backbone.sync

    Override Backbone.sync to hand off to sync function in relevant backend Backbone.sync = function(method, model, options) { return model.backend.sync(method, model, options); };

    }(this.recline.Model));