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