Merge branch 'master' of github.com:okfn/recline

Conflicts:
	src/view-map.js
This commit is contained in:
amercader
2012-04-23 11:37:51 +01:00
33 changed files with 2192 additions and 2828 deletions

View File

@@ -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) {

View File

@@ -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'
},

View File

@@ -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'

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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;
}
}
});

View File

@@ -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

View File

@@ -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('&');