diff --git a/dist/recline.css b/dist/recline.css
index fcc74387..fe26df10 100644
--- a/dist/recline.css
+++ b/dist/recline.css
@@ -204,94 +204,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit {
color: #999;
}
-
-/**********************************************************
- * Transform Dialog
- *********************************************************/
-
-textarea.expression-preview-code {
- font-family: monospace;
- height: 5em;
- vertical-align: top;
-}
-
-.expression-preview-parsing-status {
- color: #999;
-}
-
-.expression-preview-parsing-status.error {
- color: red;
-}
-
-#expression-preview-tabs-preview,
-#expression-preview-tabs-help,
-#expression-preview-tabs-history,
-#expression-preview-tabs-starred {
- padding: 5px;
- overflow: hidden;
-}
-
-#expression-preview-tabs-preview > div,
-#expression-preview-tabs-help > div,
-#expression-preview-tabs-history > div,
-#expression-preview-tabs-starred {
- height: 200px;
- overflow: auto;
-}
-
-#expression-preview-tabs-preview td, #expression-preview-tabs-preview th,
-#expression-preview-tabs-help td, #expression-preview-tabs-help th,
-#expression-preview-tabs-history td, #expression-preview-tabs-history th,
-#expression-preview-tabs-starred td, #expression-preview-tabs-starred th {
- padding: 5px;
-}
-
-.expression-preview-table-wrapper {
- padding: 7px;
-}
-
-.expression-preview-container td {
- padding: 2px 5px;
- border-top: 1px solid #ccc;
-}
-
-td.expression-preview-heading {
- border-top: none;
- background: #ddd;
- font-weight: bold;
-}
-
-td.expression-preview-value {
- max-width: 250px !important;
- overflow-x: hidden;
-}
-
-.expression-preview-special-value {
- color: #aaa;
-}
-
-.expression-preview-help-container h3 {
- margin-top: 15px;
- margin-bottom: 7px;
- border-bottom: 1px solid #999;
-}
-
-.expression-preview-doc-item-title {
- font-weight: bold;
- text-align: right;
-}
-
-.expression-preview-doc-item-params {
-}
-
-.expression-preview-doc-item-returns {
-}
-
-.expression-preview-doc-item-desc {
- color: #666;
-}
-
-
/**********************************************************
* Read-only mode
*********************************************************/
@@ -691,3 +603,42 @@ classes should alter those!
cursor: pointer;
}
+.recline-transform .script {
+ margin-right: 10px;
+}
+
+.recline-transform .script textarea {
+ width: 100%;
+ height: 100px;
+ font-family: monospace;
+}
+
+.recline-transform h2 {
+ margin-bottom: 10px;
+}
+
+.recline-transform h2 .okButton {
+ margin-left: 10px;
+ margin-top: -2px;
+}
+
+.recline-transform .preview {
+ margin-right: 10px;
+}
+
+.expression-preview-parsing-status {
+ color: #999;
+}
+
+.expression-preview-parsing-status.error {
+ color: red;
+}
+
+.recline-transform .before-after .after {
+ font-style: italic;
+}
+
+.recline-transform .before-after .after.different {
+ font-weight: bold;
+}
+
diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js
new file mode 100644
index 00000000..d10c92ee
--- /dev/null
+++ b/dist/recline.dataset.js
@@ -0,0 +1,748 @@
+// # Recline Backbone Models
+this.recline = this.recline || {};
+this.recline.Model = this.recline.Model || {};
+
+(function($, my) {
+
+// ## Dataset
+my.Dataset = Backbone.Model.extend({
+ __type__: 'Dataset',
+
+ // ### initialize
+ initialize: function() {
+ _.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.currentRecords = 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', this.query);
+ this.queryState.bind('facet:add', this.query);
+ // 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 (this.backend == recline.Backend.Memory) {
+ this.fetch();
+ }
+ },
+
+ // ### fetch
+ //
+ // Retrieve dataset and (some) records from the backend.
+ fetch: function() {
+ var self = this;
+ var dfd = $.Deferred();
+
+ if (this.backend !== recline.Backend.Memory) {
+ this.backend.fetch(this.toJSON())
+ .done(handleResults)
+ .fail(function(arguments) {
+ dfd.reject(arguments);
+ });
+ } else {
+ // special case where we have been given data directly
+ handleResults({
+ records: this.get('records'),
+ fields: this.get('fields'),
+ useMemoryStore: true
+ });
+ }
+
+ function handleResults(results) {
+ var out = self._normalizeRecordsAndFields(results.records, results.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(arguments) {
+ dfd.reject(arguments);
+ });
+ }
+
+ 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 && typeof fields[0] === 'string') {
+ // Rename duplicate fieldIds as each field name needs to be
+ // unique.
+ var seen = {};
+ fields = _.map(fields, function(field, index) {
+ // 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());
+ },
+
+ transform: function(editFunc) {
+ var self = this;
+ if (!this._store.transform) {
+ alert('Transform is not supported with this backend: ' + this.get('backend'));
+ return;
+ }
+ this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
+ this._store.transform(editFunc).done(function() {
+ // reload data as records have changed
+ self.query();
+ self.trigger('recline:flash', {message: "Records updated successfully"});
+ });
+ },
+
+ // ### 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.currentRecords and are
+ // also returned.
+ query: function(queryObj) {
+ var self = this;
+ var dfd = $.Deferred();
+ this.trigger('query:start');
+
+ if (queryObj) {
+ this.queryState.set(queryObj, {silent: true});
+ }
+ var actualQuery = this.queryState.toJSON();
+
+ this._store.query(actualQuery, this.toJSON())
+ .done(function(queryResult) {
+ self._handleQueryResult(queryResult);
+ self.trigger('query:done');
+ dfd.resolve(self.currentRecords);
+ })
+ .fail(function(arguments) {
+ self.trigger('query:fail', arguments);
+ dfd.reject(arguments);
+ });
+ 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.bind('change', function(doc) {
+ self._changes.updates.push(doc.toJSON());
+ });
+ _doc.bind('destroy', function(doc) {
+ self._changes.deletes.push(doc.toJSON());
+ });
+ return _doc;
+ });
+ self.currentRecords.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 = $.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();
+ },
+
+ // ### recordSummary
+ //
+ // Get a simple html summary of a Dataset record in form of key/value list
+ recordSummary: function(record) {
+ var html = '
';
+ this.fields.each(function(field) {
+ if (field.id != 'id') {
+ html += '
' + field.get('label') + ': ' + record.getFieldValue(field) + '
';
+ }
+ });
+ html += '
';
+ return html;
+ },
+
+ // ### _backendFromString(backendString)
+ //
+ // See backend argument to initialize for details
+ _backendFromString: function(backendString) {
+ var parts = backendString.split('.');
+ // walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
+ var current = window;
+ for(ii=0;ii
+// {
+// backend: {backend type - i.e. value of dataset.backend.__type__}
+// 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) {
+ var dataset = null;
+ // hack-y - restoring a memory dataset does not mean much ...
+ if (state.backend === 'memory') {
+ var datasetInfo = {
+ records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
+ };
+ } else {
+ var datasetInfo = {
+ url: state.url,
+ backend: state.backend
+ };
+ }
+ dataset = new recline.Model.Dataset(datasetInfo);
+ return dataset;
+};
+
+// ## A Record (aka Row)
+//
+// A single entry or row in the dataset
+my.Record = Backbone.Model.extend({
+ __type__: 'Record',
+ initialize: function() {
+ _.bindAll(this, 'getFieldValue');
+ },
+
+ // ### getFieldValue
+ //
+ // For the provided Field get the corresponding rendered computed data value
+ // for this record.
+ getFieldValue: function(field) {
+ val = this.getFieldValueUnrendered(field);
+ if (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.
+ getFieldValueUnrendered: function(field) {
+ var val = this.get(field.id);
+ if (field.deriver) {
+ val = field.deriver(val, field, this);
+ }
+ return val;
+ },
+
+ // 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({
+ __type__: 'RecordList',
+ model: my.Record
+});
+
+// ## A Field (aka Column) on a Dataset
+my.Field = Backbone.Model.extend({
+ // ### 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 (options) {
+ this.renderer = options.renderer;
+ this.deriver = options.deriver;
+ }
+ if (!this.renderer) {
+ this.renderer = this.defaultRenderers[this.get('type')];
+ }
+ this.facets = new my.FacetList();
+ },
+ defaultRenderers: {
+ object: function(val, field, doc) {
+ return JSON.stringify(val);
+ },
+ geo_point: function(val, field, doc) {
+ return JSON.stringify(val);
+ },
+ 'float': 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, '$1');
+ }
+ return val
+ }
+ }
+ }
+});
+
+my.FieldList = Backbone.Collection.extend({
+ model: my.Field
+});
+
+// ## Query
+my.Query = Backbone.Model.extend({
+ defaults: function() {
+ return {
+ size: 100,
+ from: 0,
+ q: '',
+ facets: {},
+ filters: []
+ };
+ },
+ _filterTemplates: {
+ term: {
+ type: 'term',
+ field: '',
+ term: ''
+ },
+ geo_distance: {
+ distance: 10,
+ unit: 'km',
+ point: {
+ lon: 0,
+ lat: 0
+ }
+ }
+ },
+ // ### addFilter
+ //
+ // Add a new filter (appended 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 full specified so use template and over-write
+ if (_.keys(filter).length <= 2) {
+ ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
+ }
+ var filters = this.get('filters');
+ filters.push(ourfilter);
+ this.trigger('change:filters:new-blank');
+ },
+ 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
+ addFacet: 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;
+ }
+ facets[fieldId] = {
+ terms: { field: fieldId }
+ };
+ this.set({facets: facets}, {silent: true});
+ 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);
+ }
+});
+
+
+// ## A Facet (Result)
+my.Facet = Backbone.Model.extend({
+ defaults: function() {
+ return {
+ _type: 'terms',
+ total: 0,
+ other: 0,
+ missing: 0,
+ terms: []
+ };
+ }
+});
+
+// ## A Collection/List of Facets
+my.FacetList = Backbone.Collection.extend({
+ 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);
+};
+
+}(jQuery, this.recline.Model));
+
+this.recline = this.recline || {};
+this.recline.Backend = this.recline.Backend || {};
+this.recline.Backend.Memory = this.recline.Backend.Memory || {};
+
+(function($, my) {
+ my.__type__ = 'memory';
+
+ // ## Data Wrapper
+ //
+ // Turn a simple array of JS objects into a mini data-store with
+ // functionality like querying, faceting, updating (by ID) and deleting (by
+ // ID).
+ //
+ // @param data list of hashes for each record/row in the data ({key:
+ // value, key: value})
+ // @param fields (optional) list of field hashes (each hash defining a field
+ // as per recline.Model.Field). If fields not specified they will be taken
+ // from the data.
+ my.Store = function(data, fields) {
+ var self = this;
+ this.data = data;
+ if (fields) {
+ this.fields = fields;
+ } else {
+ if (data) {
+ this.fields = _.map(data[0], function(value, key) {
+ return {id: key};
+ });
+ }
+ }
+
+ this.update = function(doc) {
+ _.each(self.data, function(internalDoc, idx) {
+ if(doc.id === internalDoc.id) {
+ self.data[idx] = doc;
+ }
+ });
+ };
+
+ this.delete = function(doc) {
+ var newdocs = _.reject(self.data, function(internalDoc) {
+ return (doc.id === internalDoc.id);
+ });
+ this.data = newdocs;
+ };
+
+ this.save = function(changes, dataset) {
+ var self = this;
+ var dfd = $.Deferred();
+ // TODO _.each(changes.creates) { ... }
+ _.each(changes.updates, function(record) {
+ self.update(record);
+ });
+ _.each(changes.deletes, function(record) {
+ self.delete(record);
+ });
+ dfd.resolve();
+ return dfd.promise();
+ },
+
+ this.query = function(queryObj) {
+ var dfd = $.Deferred();
+ var numRows = queryObj.size || this.data.length;
+ var start = queryObj.from || 0;
+ var results = this.data;
+ results = this._applyFilters(results, queryObj);
+ results = this._applyFreeTextQuery(results, queryObj);
+ // not complete sorting!
+ _.each(queryObj.sort, function(sortObj) {
+ var fieldName = _.keys(sortObj)[0];
+ results = _.sortBy(results, function(doc) {
+ var _out = doc[fieldName];
+ return _out;
+ });
+ if (sortObj[fieldName].order == 'desc') {
+ results.reverse();
+ }
+ });
+ var facets = this.computeFacets(results, queryObj);
+ var out = {
+ total: results.length,
+ hits: results.slice(start, start+numRows),
+ facets: facets
+ };
+ dfd.resolve(out);
+ return dfd.promise();
+ };
+
+ // in place filtering
+ this._applyFilters = function(results, queryObj) {
+ _.each(queryObj.filters, function(filter) {
+ // if a term filter ...
+ if (filter.type === 'term') {
+ results = _.filter(results, function(doc) {
+ return (doc[filter.field] == filter.term);
+ });
+ }
+ });
+ return results;
+ };
+
+ // we OR across fields but AND across terms in query string
+ this._applyFreeTextQuery = function(results, queryObj) {
+ if (queryObj.q) {
+ var terms = queryObj.q.split(' ');
+ results = _.filter(results, function(rawdoc) {
+ var matches = true;
+ _.each(terms, function(term) {
+ var foundmatch = false;
+ _.each(self.fields, function(field) {
+ var value = rawdoc[field.id];
+ if (value !== null) {
+ value = value.toString();
+ } else {
+ // value can be null (apparently in some cases)
+ value = '';
+ }
+ // TODO regexes?
+ foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());
+ // 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;
+ });
+ return matches;
+ });
+ }
+ return results;
+ };
+
+ this.computeFacets = function(records, queryObj) {
+ var facetResults = {};
+ if (!queryObj.facets) {
+ return facetResults;
+ }
+ _.each(queryObj.facets, function(query, facetId) {
+ // TODO: remove dependency on recline.Model
+ facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
+ facetResults[facetId].termsall = {};
+ });
+ // faceting
+ _.each(records, function(doc) {
+ _.each(queryObj.facets, function(query, facetId) {
+ var fieldId = query.terms.field;
+ var val = doc[fieldId];
+ var tmp = facetResults[facetId];
+ if (val) {
+ tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
+ } else {
+ tmp.missing = tmp.missing + 1;
+ }
+ });
+ });
+ _.each(queryObj.facets, function(query, facetId) {
+ var tmp = facetResults[facetId];
+ var terms = _.map(tmp.termsall, function(count, term) {
+ return { term: term, count: count };
+ });
+ tmp.terms = _.sortBy(terms, function(item) {
+ // want descending order
+ return -item.count;
+ });
+ tmp.terms = tmp.terms.slice(0, 10);
+ });
+ return facetResults;
+ };
+
+ this.transform = function(editFunc) {
+ var toUpdate = costco.mapDocs(this.data, editFunc);
+ // TODO: very inefficient -- could probably just walk the documents and updates in tandem and update
+ _.each(toUpdate.updates, function(record, idx) {
+ self.data[idx] = record;
+ });
+ return this.save(toUpdate);
+ };
+ };
+
+}(jQuery, this.recline.Backend.Memory));
diff --git a/dist/recline.js b/dist/recline.js
index 31717e0a..1da6ea5b 100644
--- a/dist/recline.js
+++ b/dist/recline.js
@@ -188,6 +188,9 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
my.__type__ = 'dataproxy';
// URL for the dataproxy
my.dataproxy_url = 'http://jsonpdataproxy.appspot.com';
+ // Timeout for dataproxy (after this time if no response we error)
+ // Needed because use JSONP so do not receive e.g. 500 errors
+ my.timeout = 5000;
// ## load
//
@@ -230,12 +233,11 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
// a crude way to catch those errors.
var _wrapInTimeout = function(ourFunction) {
var dfd = $.Deferred();
- var timeout = 5000;
var timer = setTimeout(function() {
dfd.reject({
- message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
+ message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
});
- }, timeout);
+ }, my.timeout);
ourFunction.done(function(arguments) {
clearTimeout(timer);
dfd.resolve(arguments);
@@ -743,7 +745,12 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
var foundmatch = false;
_.each(self.fields, function(field) {
var value = rawdoc[field.id];
- if (value !== null) { value = value.toString(); }
+ if (value !== null) {
+ value = value.toString();
+ } else {
+ // value can be null (apparently in some cases)
+ value = '';
+ }
// TODO regexes?
foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());
// TODO: early out (once we are true should break to spare unnecessary testing)
@@ -795,6 +802,15 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
});
return facetResults;
};
+
+ this.transform = function(editFunc) {
+ var toUpdate = costco.mapDocs(this.data, editFunc);
+ // TODO: very inefficient -- could probably just walk the documents and updates in tandem and update
+ _.each(toUpdate.updates, function(record, idx) {
+ self.data[idx] = record;
+ });
+ return this.save(toUpdate);
+ };
};
}(jQuery, this.recline.Backend.Memory));
@@ -820,9 +836,9 @@ var costco = function() {
;
if (!after) after = {};
if (currentColumn) {
- preview.push({before: JSON.stringify(before[currentColumn]), after: JSON.stringify(after[currentColumn])});
+ preview.push({before: before[currentColumn], after: after[currentColumn]});
} else {
- preview.push({before: JSON.stringify(before), after: JSON.stringify(after)});
+ preview.push({before: before, after: after});
}
}
return preview;
@@ -853,9 +869,9 @@ var costco = function() {
});
return {
- edited: edited,
+ updates: edited,
docs: updatedDocs,
- deleted: deleted,
+ deletes: deleted,
failed: failed
};
}
@@ -895,7 +911,7 @@ my.Dataset = Backbone.Model.extend({
creates: []
};
this.facets = new my.FacetList();
- this.docCount = null;
+ this.recordCount = null;
this.queryState = new my.Query();
this.queryState.bind('change', this.query);
this.queryState.bind('facet:add', this.query);
@@ -1017,6 +1033,20 @@ my.Dataset = Backbone.Model.extend({
return this._store.save(this._changes, this.toJSON());
},
+ transform: function(editFunc) {
+ var self = this;
+ if (!this._store.transform) {
+ alert('Transform is not supported with this backend: ' + this.get('backend'));
+ return;
+ }
+ this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
+ this._store.transform(editFunc).done(function() {
+ // reload data as records have changed
+ self.query();
+ self.trigger('recline:flash', {message: "Records updated successfully"});
+ });
+ },
+
// ### query
//
// AJAX method with promise API to get records from the backend.
@@ -1032,7 +1062,7 @@ my.Dataset = Backbone.Model.extend({
this.trigger('query:start');
if (queryObj) {
- this.queryState.set(queryObj);
+ this.queryState.set(queryObj, {silent: true});
}
var actualQuery = this.queryState.toJSON();
@@ -1051,7 +1081,7 @@ my.Dataset = Backbone.Model.extend({
_handleQueryResult: function(queryResult) {
var self = this;
- self.docCount = queryResult.total;
+ self.recordCount = queryResult.total;
var docs = _.map(queryResult.hits, function(hit) {
var _doc = new my.Record(hit);
_doc.bind('change', function(doc) {
@@ -1074,11 +1104,13 @@ my.Dataset = Backbone.Model.extend({
toTemplateJSON: function() {
var data = this.toJSON();
- data.docCount = this.docCount;
+ 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.
@@ -1104,6 +1136,20 @@ my.Dataset = Backbone.Model.extend({
return dfd.promise();
},
+ // ### recordSummary
+ //
+ // Get a simple html summary of a Dataset record in form of key/value list
+ recordSummary: function(record) {
+ var html = '';
+ this.fields.each(function(field) {
+ if (field.id != 'id') {
+ html += '
' + field.get('label') + ': ' + record.getFieldValue(field) + '
';
+ }
+ });
+ html += '
';
+ return html;
+ },
+
// ### _backendFromString(backendString)
//
// See backend argument to initialize for details
@@ -1198,16 +1244,6 @@ my.Record = Backbone.Model.extend({
return val;
},
- summary: function(fields) {
- var html = '';
- for (key in this.attributes) {
- if (key != 'id') {
- html += '' + key + ': '+ this.attributes[key] + '
';
- }
- }
- 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)
@@ -1258,6 +1294,9 @@ my.Field = Backbone.Model.extend({
object: function(val, field, doc) {
return JSON.stringify(val);
},
+ geo_point: function(val, field, doc) {
+ return JSON.stringify(val);
+ },
'float': function(val, field, doc) {
var format = field.get('format');
if (format === 'percentage') {
@@ -2254,7 +2293,7 @@ my.Map = Backbone.View.extend({
// If not found, the user will need to define the fields via the editor.
latitudeFieldNames: ['lat','latitude'],
longitudeFieldNames: ['lon','longitude'],
- geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location'],
+ geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
initialize: function(options) {
var self = this;
@@ -2856,7 +2895,7 @@ my.MultiView = Backbone.View.extend({
\
\
\
- Results found {{docCount}} \
+ {{recordCount}} records\
\