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', __type__: 'base',Class level attribute indicating that this backend is read-only (that +is, cannot be written to).
readonly: true,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 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) {
+ },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
};
- },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');
}
+ },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 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'
+ });
+ },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.
etc ...
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. 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? 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. 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. 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'