Merge branch 'master' of github.com:okfn/recline
Conflicts: src/view-map.js
This commit is contained in:
@@ -32,6 +32,13 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// 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
|
||||
@@ -92,6 +99,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:
|
||||
//
|
||||
// <pre>
|
||||
// var jqxhr = this._makeRequest({
|
||||
// url: the-url
|
||||
// });
|
||||
// </pre>
|
||||
_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) {
|
||||
|
||||
@@ -18,6 +18,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// Note that this is a **read-only** backend.
|
||||
my.DataProxy = my.Base.extend({
|
||||
__type__: 'dataproxy',
|
||||
readonly: true,
|
||||
defaults: {
|
||||
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
|
||||
},
|
||||
|
||||
@@ -6,36 +6,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:
|
||||
//
|
||||
// <pre>
|
||||
// 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:
|
||||
//
|
||||
// <pre>http://localhost:9200/twitter/tweet</pre>
|
||||
//
|
||||
// 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):
|
||||
//
|
||||
// <pre>
|
||||
// elasticsearch_url
|
||||
// webstore_url
|
||||
// url
|
||||
// </pre>
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// <pre>http://localhost:9200/twitter/tweet</pre>
|
||||
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'
|
||||
});
|
||||
@@ -54,11 +57,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() === '') {
|
||||
@@ -95,7 +164,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'
|
||||
|
||||
@@ -18,6 +18,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// </pre>
|
||||
my.GDoc = my.Base.extend({
|
||||
__type__: 'gdoc',
|
||||
readonly: true,
|
||||
getUrl: function(dataset) {
|
||||
var url = dataset.get('url');
|
||||
if (url.indexOf('feeds/list') != -1) {
|
||||
|
||||
@@ -71,6 +71,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// </pre>
|
||||
my.Memory = my.Base.extend({
|
||||
__type__: 'memory',
|
||||
readonly: false,
|
||||
initialize: function() {
|
||||
this.datasets = {};
|
||||
},
|
||||
@@ -158,7 +159,8 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
_.each(terms, function(term) {
|
||||
var foundmatch = false;
|
||||
dataset.fields.each(function(field) {
|
||||
var value = rawdoc[field.id].toString();
|
||||
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)
|
||||
|
||||
28
src/model.js
28
src/model.js
@@ -150,17 +150,22 @@ my.Dataset = Backbone.Model.extend({
|
||||
// <pre>
|
||||
// {
|
||||
// backend: {backend type - i.e. value of dataset.backend.__type__}
|
||||
// dataset: {result of dataset.toJSON()}
|
||||
// dataset: {dataset info needed for loading -- result of dataset.toJSON() would be sufficient but can be simpler }
|
||||
// // convenience - if url provided and dataste not this be used as dataset url
|
||||
// url: {dataset url}
|
||||
// ...
|
||||
// }
|
||||
my.Dataset.restore = function(state) {
|
||||
// hack-y - restoring a memory dataset does not mean much ...
|
||||
var dataset = null;
|
||||
if (state.url && !state.dataset) {
|
||||
state.dataset = {url: state.url};
|
||||
}
|
||||
if (state.backend === 'memory') {
|
||||
dataset = recline.Backend.createDataset(
|
||||
[{stub: 'this is a stub dataset because we do not restore memory datasets'}],
|
||||
[],
|
||||
state.dataset
|
||||
state.dataset // metadata
|
||||
);
|
||||
} else {
|
||||
dataset = new recline.Model.Dataset(
|
||||
@@ -212,7 +217,8 @@ my.DocumentList = Backbone.Collection.extend({
|
||||
// * 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).
|
||||
//
|
||||
// Following additional instance properties:
|
||||
@@ -268,6 +274,22 @@ my.Field = Backbone.Model.extend({
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -280,9 +280,12 @@ my.Map = Backbone.View.extend({
|
||||
|
||||
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'){
|
||||
if (typeof feature === 'undefined' || feature === null){
|
||||
// Empty field
|
||||
return true;
|
||||
} else if (feature instanceof Object){
|
||||
@@ -299,16 +302,20 @@ my.Map = Backbone.View.extend({
|
||||
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;
|
||||
});
|
||||
@@ -341,13 +348,17 @@ my.Map = Backbone.View.extend({
|
||||
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')]
|
||||
]
|
||||
};
|
||||
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;
|
||||
}
|
||||
@@ -359,12 +370,16 @@ my.Map = Backbone.View.extend({
|
||||
// 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')));
|
||||
// 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
|
||||
|
||||
70
src/view.js
70
src/view.js
@@ -184,6 +184,7 @@ my.DataExplorer = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
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;
|
||||
@@ -192,26 +193,37 @@ my.DataExplorer = Backbone.View.extend({
|
||||
id: 'grid',
|
||||
label: 'Grid',
|
||||
view: new my.Grid({
|
||||
model: this.model
|
||||
})
|
||||
model: this.model,
|
||||
state: this.state.get('view-grid')
|
||||
}),
|
||||
}, {
|
||||
id: 'graph',
|
||||
label: 'Graph',
|
||||
view: new my.Graph({
|
||||
model: this.model
|
||||
})
|
||||
model: this.model,
|
||||
state: this.state.get('view-graph')
|
||||
}),
|
||||
}, {
|
||||
id: 'map',
|
||||
label: 'Map',
|
||||
view: new my.Map({
|
||||
model: this.model
|
||||
})
|
||||
model: this.model,
|
||||
state: this.state.get('view-map')
|
||||
}),
|
||||
}];
|
||||
}
|
||||
// these must be called after pageViews are created
|
||||
this.render();
|
||||
// should come after render as may need to interact with elements in the view
|
||||
this._setupState(options.state);
|
||||
this._bindStateChanges();
|
||||
// now do updates based on state (need to come after render)
|
||||
if (this.state.get('readOnly')) {
|
||||
this.setReadOnly();
|
||||
}
|
||||
if (this.state.get('currentView')) {
|
||||
this.updateNav(this.state.get('currentView'));
|
||||
} else {
|
||||
this.updateNav(this.pageViews[0].id);
|
||||
}
|
||||
|
||||
this.router = new Backbone.Router();
|
||||
this.setupRouting();
|
||||
@@ -227,7 +239,7 @@ my.DataExplorer = Backbone.View.extend({
|
||||
var qs = my.parseHashQueryString();
|
||||
qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
|
||||
var out = my.getNewHashForQueryString(qs);
|
||||
self.router.navigate(out);
|
||||
// self.router.navigate(out);
|
||||
});
|
||||
this.model.bind('query:fail', function(error) {
|
||||
my.clearNotifications();
|
||||
@@ -290,13 +302,15 @@ my.DataExplorer = Backbone.View.extend({
|
||||
setupRouting: function() {
|
||||
var self = this;
|
||||
// Default route
|
||||
this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
|
||||
self.updateNav(self.pageViews[0].id, queryString);
|
||||
});
|
||||
$.each(this.pageViews, function(idx, view) {
|
||||
self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
|
||||
self.updateNav(viewId, queryString);
|
||||
});
|
||||
// this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
|
||||
// self.updateNav(self.pageViews[0].id, queryString);
|
||||
// });
|
||||
// $.each(this.pageViews, function(idx, view) {
|
||||
// self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
|
||||
// self.updateNav(viewId, queryString);
|
||||
// });
|
||||
// });
|
||||
this.router.route(/.*/, 'view', function() {
|
||||
});
|
||||
},
|
||||
|
||||
@@ -356,29 +370,18 @@ my.DataExplorer = Backbone.View.extend({
|
||||
'view-graph': graphState,
|
||||
backend: this.model.backend.__type__,
|
||||
dataset: this.model.toJSON(),
|
||||
currentView: this.pageViews[0].id,
|
||||
currentView: null,
|
||||
readOnly: false
|
||||
},
|
||||
initialState);
|
||||
this.state = new recline.Model.ObjectState(stateData);
|
||||
},
|
||||
|
||||
// now do updates based on state
|
||||
if (this.state.get('readOnly')) {
|
||||
this.setReadOnly();
|
||||
}
|
||||
if (this.state.get('currentView')) {
|
||||
this.updateNav(this.state.get('currentView'));
|
||||
}
|
||||
_.each(this.pageViews, function(pageView) {
|
||||
var viewId = 'view-' + pageView.id;
|
||||
if (viewId in self.state.attributes) {
|
||||
pageView.view.state.set(self.state.get(viewId));
|
||||
}
|
||||
});
|
||||
|
||||
_bindStateChanges: function() {
|
||||
var self = this;
|
||||
// finally ensure we update our state object when state of sub-object changes so that state is always up to date
|
||||
this.model.queryState.bind('change', function() {
|
||||
self.state.set({queryState: self.model.queryState.toJSON()});
|
||||
self.state.set({query: self.model.queryState.toJSON()});
|
||||
});
|
||||
_.each(this.pageViews, function(pageView) {
|
||||
if (pageView.view.state && pageView.view.state.bind) {
|
||||
@@ -682,6 +685,9 @@ my.composeQueryString = function(queryParams) {
|
||||
var queryString = '?';
|
||||
var items = [];
|
||||
$.each(queryParams, function(key, value) {
|
||||
if (typeof(value) === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
items.push(key + '=' + value);
|
||||
});
|
||||
queryString += items.join('&');
|
||||
|
||||
Reference in New Issue
Block a user