diff --git a/docs/backend/base.html b/docs/backend/base.html index 9f022974..38e13e3a 100644 --- a/docs/backend/base.html +++ b/docs/backend/base.html @@ -22,7 +22,10 @@ object as a string (e.g. recline.Backend.Memory) or for Backends within recline.Backend module it may be their class name.

This value is used as an identifier for this backend when initializing -backends (see recline.Model.Dataset.initialize).

    __type__: 'base',

sync

+backends (see recline.Model.Dataset.initialize).

    __type__: 'base',

readonly

+ +

Class level attribute indicating that this backend is read-only (that +is, cannot be written to).

    readonly: true,

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.

@@ -36,7 +39,7 @@ for Documents because they are loaded in bulk by the query method.

All code paths should return an object conforming to the jquery promise API.

    sync: function(method, model, options) {
     },
-    

query

+

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, @@ -77,7 +80,30 @@ details):

} }
    query: function(model, queryObj) {
-    },

convenience method to convert simple set of documents / rows to a QueryResult

    _docsToQueryResult: function(rows) {
+    },

_makeRequest

+ +

Just $.ajax but in any headers in the 'headers' attribute of this +Backend instance. Example:

+ +
+var jqxhr = this._makeRequest({
+  url: the-url
+});
+
    _makeRequest: function(data) {
+      var headers = this.get('headers');
+      var extras = {};
+      if (headers) {
+        extras = {
+          beforeSend: function(req) {
+            _.each(headers, function(value, key) {
+              req.setRequestHeader(key, value);
+            });
+          }
+        };
+      }
+      var data = _.extend(extras, data);
+      return $.ajax(data);
+    },

convenience method to convert simple set of documents / rows to a QueryResult

    _docsToQueryResult: function(rows) {
       var hits = _.map(rows, function(row) {
         return { _source: row };
       });
@@ -85,7 +111,7 @@ details):

total: null, hits: hits }; - },

_wrapInTimeout

+ },

_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 diff --git a/docs/backend/dataproxy.html b/docs/backend/dataproxy.html index b65ef570..0ff29c43 100644 --- a/docs/backend/dataproxy.html +++ b/docs/backend/dataproxy.html @@ -20,6 +20,7 @@

Note that this is a read-only backend.

  my.DataProxy = my.Base.extend({
     __type__: 'dataproxy',
+    readonly: true,
     defaults: {
       dataproxy_url: 'http://jsonpdataproxy.appspot.com'
     },
diff --git a/docs/backend/elasticsearch.html b/docs/backend/elasticsearch.html
index 0257f24c..11bb4b3a 100644
--- a/docs/backend/elasticsearch.html
+++ b/docs/backend/elasticsearch.html
@@ -5,35 +5,38 @@
 
 

Connecting to ElasticSearch.

-

To use this backend ensure your Dataset has one of the following -attributes (first one found is used):

+

Usage:

+ +
+var backend = new recline.Backend.ElasticSearch({
+  // optional as can also be provided by Dataset/Document
+  url: {url to ElasticSearch endpoint i.e. ES 'type/table' url - more info below}
+  // optional
+  headers: {dict of headers to add to each request}
+});
+
+@param {String} url: url for ElasticSearch type/table, e.g. for ES running
+on localhost:9200 with index // twitter and type tweet it would be:
+
+
http://localhost:9200/twitter/tweet
+ +This url is optional since the ES endpoint url may be specified on the the +dataset (and on a Document by the document having a dataset attribute) by +having one of the following (see also `_getESUrl` function):
 elasticsearch_url
 webstore_url
 url
-
- -

This should point to the ES type url. E.G. for ES running on -localhost:9200 with index twitter and type tweet it would be

- -
http://localhost:9200/twitter/tweet
  my.ElasticSearch = my.Base.extend({
+
  my.ElasticSearch = my.Base.extend({
     __type__: 'elasticsearch',
-    _getESUrl: function(dataset) {
-      var out = dataset.get('elasticsearch_url');
-      if (out) return out;
-      out = dataset.get('webstore_url');
-      if (out) return out;
-      out = dataset.get('url');
-      return out;
-    },
+    readonly: false,
     sync: function(method, model, options) {
       var self = this;
       if (method === "read") {
         if (model.__type__ == 'Dataset') {
-          var base = self._getESUrl(model);
-          var schemaUrl = base + '/_mapping';
-          var jqxhr = $.ajax({
+          var schemaUrl = self._getESUrl(model) + '/_mapping';
+          var jqxhr = this._makeRequest({
             url: schemaUrl,
             dataType: 'jsonp'
           });
@@ -50,10 +53,67 @@ localhost:9200 with index twitter and type tweet it would be

dfd.reject(arguments); }); return dfd.promise(); + } else if (model.__type__ == 'Document') { + var base = this._getESUrl(model.dataset) + '/' + model.id; + return this._makeRequest({ + url: base, + dataType: 'json' + }); + } + } else if (method === 'update') { + if (model.__type__ == 'Document') { + return this.upsert(model.toJSON(), this._getESUrl(model.dataset)); + } + } else if (method === 'delete') { + if (model.__type__ == 'Document') { + var url = this._getESUrl(model.dataset); + return this.delete(model.id, url); } - } else { - alert('This backend currently only supports read operations'); } + },

upsert

+ +

create / update a document to ElasticSearch backend

+ +

@param {Object} doc an object to insert to the index. +@param {string} url (optional) url for ElasticSearch endpoint (if not +defined called this._getESUrl()

    upsert: function(doc, url) {
+      var data = JSON.stringify(doc);
+      url = url ? url : this._getESUrl();
+      if (doc.id) {
+        url += '/' + doc.id;
+      }
+      return this._makeRequest({
+        url: url,
+        type: 'POST',
+        data: data,
+        dataType: 'json'
+      });
+    },

delete

+ +

Delete a document from the ElasticSearch backend.

+ +

@param {Object} id id of object to delete +@param {string} url (optional) url for ElasticSearch endpoint (if not +provided called this._getESUrl()

    delete: function(id, url) {
+      url = url ? url : this._getESUrl();
+      url += '/' + id;
+      return this._makeRequest({
+        url: url,
+        type: 'DELETE',
+        dataType: 'json'
+      });
+    },

_getESUrl

+ +

get url to ElasticSearch endpoint (see above)

    _getESUrl: function(dataset) {
+      if (dataset) {
+        var out = dataset.get('elasticsearch_url');
+        if (out) return out;
+        out = dataset.get('webstore_url');
+        if (out) return out;
+        out = dataset.get('url');
+        return out;
+      }
+      return this.get('url');
     },
     _normalizeQuery: function(queryObj) {
       var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
@@ -71,7 +131,7 @@ localhost:9200 with index twitter and type tweet it would be

} }; delete out.q; - }

now do filters (note the plural)

      if (out.filters && out.filters.length) {
+      }

now do filters (note the plural)

      if (out.filters && out.filters.length) {
         if (!out.filter) {
           out.filter = {};
         }
@@ -89,12 +149,12 @@ localhost:9200 with index twitter and type tweet it would be

var queryNormalized = this._normalizeQuery(queryObj); var data = {source: JSON.stringify(queryNormalized)}; var base = this._getESUrl(model); - var jqxhr = $.ajax({ + var jqxhr = this._makeRequest({ url: base + '/_search', data: data, dataType: 'jsonp' }); - var dfd = $.Deferred();

TODO: fail case

      jqxhr.done(function(results) {
+      var dfd = $.Deferred();

TODO: fail case

      jqxhr.done(function(results) {
         _.each(results.hits.hits, function(hit) {
           if (!('id' in hit._source) && hit._id) {
             hit._source.id = hit._id;
diff --git a/docs/backend/gdocs.html b/docs/backend/gdocs.html
index 4d23ae12..b84397fa 100644
--- a/docs/backend/gdocs.html
+++ b/docs/backend/gdocs.html
@@ -16,6 +16,7 @@ var dataset = new recline.Model.Dataset({
 );
 
  my.GDoc = my.Base.extend({
     __type__: 'gdoc',
+    readonly: true,
     getUrl: function(dataset) {
       var url = dataset.get('url');
       if (url.indexOf('feeds/list') != -1) {
diff --git a/docs/backend/memory.html b/docs/backend/memory.html
index b55f36f9..6304cee6 100644
--- a/docs/backend/memory.html
+++ b/docs/backend/memory.html
@@ -67,6 +67,7 @@ If not defined (or id not provided) id will be autogenerated.

  my.Memory = my.Base.extend({
     __type__: 'memory',
+    readonly: false,
     initialize: function() {
       this.datasets = {};
     },
@@ -146,7 +147,8 @@ If not defined (or id not provided) id will be autogenerated.

_.each(terms, function(term) { var foundmatch = false; dataset.fields.each(function(field) { - var value = rawdoc[field.id].toString();

TODO regexes?

              foundmatch = foundmatch || (value === term);

TODO: early out (once we are true should break to spare unnecessary testing) + var value = rawdoc[field.id]; + if (value !== null) { value = value.toString(); }

TODO regexes?

              foundmatch = foundmatch || (value === term);

TODO: early out (once we are true should break to spare unnecessary testing) if (foundmatch) return true;

            });
             matches = matches && foundmatch;

TODO: early out (once false should break to spare unnecessary testing) if (!matches) return false;

          });
diff --git a/docs/model.html b/docs/model.html
index c43556ca..3009088a 100644
--- a/docs/model.html
+++ b/docs/model.html
@@ -183,7 +183,8 @@ for this document.

  • 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='###,###.##'
  • +
  • type=string, format=link (render as hyperlink)
  • +
  • type=string, format=markdown (render as markdown if Showdown available)
  • is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).
  • @@ -233,6 +234,22 @@ value of this field prior to rendering.

    if (format === 'percentage') { return val + '%'; } + return val; + }, + 'string': function(val, field, doc) { + var format = field.get('format'); + if (format === 'link') { + return '<a href="VAL">VAL</a>'.replace(/VAL/g, val); + } else if (format === 'markdown') { + if (typeof Showdown !== 'undefined') { + var showdown = new Showdown.converter(); + out = showdown.makeHtml(val); + return out; + } else { + return val; + } + } + return val; } } }); diff --git a/docs/view-map.html b/docs/view-map.html index b865f6e7..73faab44 100644 --- a/docs/view-map.html +++ b/docs/view-map.html @@ -217,9 +217,12 @@ stopped and an error notification shown.

    if (!(docs instanceof Array)) docs = [docs]; + var count = 0; + var wrongSoFar = 0; _.every(docs,function(doc){ + count += 1; var feature = self._getGeometryFromDocument(doc); - if (typeof feature === 'undefined'){

    Empty field

            return true;
    +      if (typeof feature === 'undefined' || feature === null){

    Empty field

            return true;
           } else if (feature instanceof Object){

    Build popup contents TODO: mustache?

            html = ''
             for (key in doc.attributes){
    @@ -229,16 +232,20 @@ TODO: mustache?

    link this Leaflet layer to a Recline doc

            feature.properties.cid = doc.cid;
     
             try {
    -            self.features.addGeoJSON(feature);
    +          self.features.addGeoJSON(feature);
             } catch (except) {
    -            var msg = 'Wrong geometry value';
    -            if (except.message) msg += ' (' + except.message + ')';
    +          wrongSoFar += 1;
    +          var msg = 'Wrong geometry value';
    +          if (except.message) msg += ' (' + except.message + ')';
    +          if (wrongSoFar <= 10) {
                 my.notify(msg,{category:'error'});
    -            return false;
    +          }
             }
           } else {
    -        my.notify('Wrong geometry value',{category:'error'});
    -        return false;
    +        wrongSoFar += 1
    +        if (wrongSoFar <= 10) {
    +          my.notify('Wrong geometry value',{category:'error'});
    +        }
           }
           return true;
         });
    @@ -259,13 +266,17 @@ link this Leaflet layer to a Recline doc

    },

    Private: Return a GeoJSON geomtry extracted from the document fields

      _getGeometryFromDocument: function(doc){
         if (this.geomReady){
           if (this.state.get('geomField')){

    We assume that the contents of the field are a valid GeoJSON object

            return doc.attributes[this.state.get('geomField')];
    -      } else if (this.state.get('lonField') && this.state.get('latField')){

    We'll create a GeoJSON like point object from the two lat/lon fields

            return {
    -          type: 'Point',
    -          coordinates: [
    -            doc.attributes[this.state.get('lonField')],
    -            doc.attributes[this.state.get('latField')]
    -            ]
    -        };
    +      } else if (this.state.get('lonField') && this.state.get('latField')){

    We'll create a GeoJSON like point object from the two lat/lon fields

            var lon = doc.get(this.state.get('lonField'));
    +        var lat = doc.get(this.state.get('latField'));
    +        if (lon && lat) {
    +          return {
    +            type: 'Point',
    +            coordinates: [
    +              doc.attributes[this.state.get('lonField')],
    +              doc.attributes[this.state.get('latField')]
    +              ]
    +          };
    +        }
           }
           return null;
         }
    @@ -274,13 +285,15 @@ two fields with lat/lon values.

    If not found, the user can define them via the UI form.

      _setupGeometryField: function(){
         var geomField, latField, lonField;
    -    this.state.set({
    -      geomField: this._checkField(this.geometryFieldNames),
    -      latField: this._checkField(this.latitudeFieldNames),
    -      lonField: this._checkField(this.longitudeFieldNames)
    -    });
    -    this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
    -  },

    Private: Check if a field in the current model exists in the provided + this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));

    should not overwrite if we have already set this (e.g. explicitly via state)

        if (!this.geomReady) {
    +      this.state.set({
    +        geomField: this._checkField(this.geometryFieldNames),
    +        latField: this._checkField(this.latitudeFieldNames),
    +        lonField: this._checkField(this.longitudeFieldNames)
    +      });
    +      this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
    +    }
    +  },

    Private: Check if a field in the current model exists in the provided list of names.

      _checkField: function(fieldNames){
         var field;
         var modelFieldNames = this.model.fields.pluck('id');
    @@ -291,7 +304,7 @@ list of names.

    } } return null; - },

    Private: Sets up the Leaflet map control and the features layer.

    + },

    Private: Sets up the Leaflet map control and the features layer.

    The map uses a base layer from MapQuest based on OpenStreetMap.

      _setupMap: function(){
    @@ -318,7 +331,7 @@ on OpenStreetMap.

    this.map.setView(new L.LatLng(0, 0), 2); this.mapReady = true; - },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
    +  },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
         var options = $('.' + id + ' > select > option');
         if (options){
           options.each(function(opt){
    diff --git a/docs/view.html b/docs/view.html
    index 0ca33a89..34f9cb34 100644
    --- a/docs/view.html
    +++ b/docs/view.html
    @@ -180,8 +180,8 @@ initialized the DataExplorer with the relevant views themselves.

    initialize: function(options) { var self = this; - this.el = $(this.el);

    Hash of 'page' views (i.e. those for whole page) keyed by page name

        this._setupState(options.state);
    -    if (options.views) {
    +    this.el = $(this.el);
    +    this._setupState(options.state);

    Hash of 'page' views (i.e. those for whole page) keyed by page name

        if (options.views) {
           this.pageViews = options.views;
         } else {
           this.pageViews = [{
    diff --git a/recline.js b/recline.js
    index a0efe02e..391296aa 100644
    --- a/recline.js
    +++ b/recline.js
    @@ -2871,6 +2871,32 @@ this.recline.Backend = this.recline.Backend || {};
         query: function(model, queryObj) {
         },
     
    +    // ### _makeRequest
    +    // 
    +    // Just $.ajax but in any headers in the 'headers' attribute of this
    +    // Backend instance. Example:
    +    //
    +    // 
    +    // var jqxhr = this._makeRequest({
    +    //   url: the-url
    +    // });
    +    // 
    + _makeRequest: function(data) { + var headers = this.get('headers'); + var extras = {}; + if (headers) { + extras = { + beforeSend: function(req) { + _.each(headers, function(value, key) { + req.setRequestHeader(key, value); + }); + } + }; + } + var data = _.extend(extras, data); + return $.ajax(data); + }, + // convenience method to convert simple set of documents / rows to a QueryResult _docsToQueryResult: function(rows) { var hits = _.map(rows, function(row) { @@ -2995,37 +3021,39 @@ this.recline.Backend = this.recline.Backend || {}; // // Connecting to [ElasticSearch](http://www.elasticsearch.org/). // - // To use this backend ensure your Dataset has one of the following - // attributes (first one found is used): + // Usage: + // + //
    +  // var backend = new recline.Backend.ElasticSearch({
    +  //   // optional as can also be provided by Dataset/Document
    +  //   url: {url to ElasticSearch endpoint i.e. ES 'type/table' url - more info below}
    +  //   // optional
    +  //   headers: {dict of headers to add to each request}
    +  // });
    +  //
    +  // @param {String} url: url for ElasticSearch type/table, e.g. for ES running
    +  // on localhost:9200 with index // twitter and type tweet it would be:
    +  // 
    +  // 
    http://localhost:9200/twitter/tweet
    + // + // This url is optional since the ES endpoint url may be specified on the the + // dataset (and on a Document by the document having a dataset attribute) by + // having one of the following (see also `_getESUrl` function): // //
       // elasticsearch_url
       // webstore_url
       // url
       // 
    - // - // This should point to the ES type url. E.G. for ES running on - // localhost:9200 with index twitter and type tweet it would be - // - //
    http://localhost:9200/twitter/tweet
    my.ElasticSearch = my.Base.extend({ __type__: 'elasticsearch', - readonly: true, - _getESUrl: function(dataset) { - var out = dataset.get('elasticsearch_url'); - if (out) return out; - out = dataset.get('webstore_url'); - if (out) return out; - out = dataset.get('url'); - return out; - }, + readonly: false, sync: function(method, model, options) { var self = this; if (method === "read") { if (model.__type__ == 'Dataset') { - var base = self._getESUrl(model); - var schemaUrl = base + '/_mapping'; - var jqxhr = $.ajax({ + var schemaUrl = self._getESUrl(model) + '/_mapping'; + var jqxhr = this._makeRequest({ url: schemaUrl, dataType: 'jsonp' }); @@ -3044,11 +3072,77 @@ this.recline.Backend = this.recline.Backend || {}; dfd.reject(arguments); }); return dfd.promise(); + } else if (model.__type__ == 'Document') { + var base = this._getESUrl(model.dataset) + '/' + model.id; + return this._makeRequest({ + url: base, + dataType: 'json' + }); + } + } else if (method === 'update') { + if (model.__type__ == 'Document') { + return this.upsert(model.toJSON(), this._getESUrl(model.dataset)); + } + } else if (method === 'delete') { + if (model.__type__ == 'Document') { + var url = this._getESUrl(model.dataset); + return this.delete(model.id, url); } - } else { - alert('This backend currently only supports read operations'); } }, + + // ### upsert + // + // create / update a document to ElasticSearch backend + // + // @param {Object} doc an object to insert to the index. + // @param {string} url (optional) url for ElasticSearch endpoint (if not + // defined called this._getESUrl() + upsert: function(doc, url) { + var data = JSON.stringify(doc); + url = url ? url : this._getESUrl(); + if (doc.id) { + url += '/' + doc.id; + } + return this._makeRequest({ + url: url, + type: 'POST', + data: data, + dataType: 'json' + }); + }, + + // ### delete + // + // Delete a document from the ElasticSearch backend. + // + // @param {Object} id id of object to delete + // @param {string} url (optional) url for ElasticSearch endpoint (if not + // provided called this._getESUrl() + delete: function(id, url) { + url = url ? url : this._getESUrl(); + url += '/' + id; + return this._makeRequest({ + url: url, + type: 'DELETE', + dataType: 'json' + }); + }, + + // ### _getESUrl + // + // get url to ElasticSearch endpoint (see above) + _getESUrl: function(dataset) { + if (dataset) { + var out = dataset.get('elasticsearch_url'); + if (out) return out; + out = dataset.get('webstore_url'); + if (out) return out; + out = dataset.get('url'); + return out; + } + return this.get('url'); + }, _normalizeQuery: function(queryObj) { var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj); if (out.q !== undefined && out.q.trim() === '') { @@ -3085,7 +3179,7 @@ this.recline.Backend = this.recline.Backend || {}; var queryNormalized = this._normalizeQuery(queryObj); var data = {source: JSON.stringify(queryNormalized)}; var base = this._getESUrl(model); - var jqxhr = $.ajax({ + var jqxhr = this._makeRequest({ url: base + '/_search', data: data, dataType: 'jsonp'