active_admin_csv_import 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,182 @@
1
+ this.recline = this.recline || {};
2
+ this.recline.Backend = this.recline.Backend || {};
3
+ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
4
+
5
+ (function(my) {
6
+ // ## fetch
7
+ //
8
+ // 3 options
9
+ //
10
+ // 1. CSV local fileobject -> HTML5 file object + CSV parser
11
+ // 2. Already have CSV string (in data) attribute -> CSV parser
12
+ // 2. online CSV file that is ajax-able -> ajax + csv parser
13
+ //
14
+ // All options generates similar data and give a memory store outcome
15
+ my.fetch = function(dataset) {
16
+ var dfd = $.Deferred();
17
+ if (dataset.file) {
18
+ var reader = new FileReader();
19
+ var encoding = dataset.encoding || 'UTF-8';
20
+ reader.onload = function(e) {
21
+ var rows = my.parseCSV(e.target.result, dataset);
22
+ dfd.resolve({
23
+ records: rows,
24
+ metadata: {
25
+ filename: dataset.file.name
26
+ },
27
+ useMemoryStore: true
28
+ });
29
+ };
30
+ reader.onerror = function (e) {
31
+ alert('Failed to load file. Code: ' + e.target.error.code);
32
+ };
33
+ reader.readAsText(dataset.file, encoding);
34
+ } else if (dataset.data) {
35
+ var rows = my.parseCSV(dataset.data, dataset);
36
+ dfd.resolve({
37
+ records: rows,
38
+ useMemoryStore: true
39
+ });
40
+ } else if (dataset.url) {
41
+ $.get(dataset.url).done(function(data) {
42
+ var rows = my.parseCSV(data, dataset);
43
+ dfd.resolve({
44
+ records: rows,
45
+ useMemoryStore: true
46
+ });
47
+ });
48
+ }
49
+ return dfd.promise();
50
+ };
51
+
52
+ // Converts a Comma Separated Values string into an array of arrays.
53
+ // Each line in the CSV becomes an array.
54
+ //
55
+ // Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.
56
+ //
57
+ // @return The CSV parsed as an array
58
+ // @type Array
59
+ //
60
+ // @param {String} s The string to convert
61
+ // @param {Object} options Options for loading CSV including
62
+ // @param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
63
+ // @param {String} [separator=','] Separator for CSV file
64
+ // Heavily based on uselesscode's JS CSV parser (MIT Licensed):
65
+ // http://www.uselesscode.org/javascript/csv/
66
+ my.parseCSV= function(s, options) {
67
+ // Get rid of any trailing \n
68
+ s = chomp(s);
69
+
70
+ var options = options || {};
71
+ var trm = (options.trim === false) ? false : true;
72
+ var separator = options.separator || ',';
73
+ var delimiter = options.delimiter || '"';
74
+
75
+ var cur = '', // The character we are currently processing.
76
+ inQuote = false,
77
+ fieldQuoted = false,
78
+ field = '', // Buffer for building up the current field
79
+ row = [],
80
+ out = [],
81
+ i,
82
+ processField;
83
+
84
+ processField = function (field) {
85
+ if (fieldQuoted !== true) {
86
+ // If field is empty set to null
87
+ if (field === '') {
88
+ field = null;
89
+ // If the field was not quoted and we are trimming fields, trim it
90
+ } else if (trm === true) {
91
+ field = trim(field);
92
+ }
93
+
94
+ // Convert unquoted numbers to their appropriate types
95
+ if (rxIsInt.test(field)) {
96
+ field = parseInt(field, 10);
97
+ } else if (rxIsFloat.test(field)) {
98
+ field = parseFloat(field, 10);
99
+ }
100
+ }
101
+ return field;
102
+ };
103
+
104
+ for (i = 0; i < s.length; i += 1) {
105
+ cur = s.charAt(i);
106
+
107
+ // If we are at a EOF or EOR
108
+ if (inQuote === false && (cur === separator || cur === "\n")) {
109
+ field = processField(field);
110
+ // Add the current field to the current row
111
+ row.push(field);
112
+ // If this is EOR append row to output and flush row
113
+ if (cur === "\n") {
114
+ out.push(row);
115
+ row = [];
116
+ }
117
+ // Flush the field buffer
118
+ field = '';
119
+ fieldQuoted = false;
120
+ } else {
121
+ // If it's not a delimiter, add it to the field buffer
122
+ if (cur !== delimiter) {
123
+ field += cur;
124
+ } else {
125
+ if (!inQuote) {
126
+ // We are not in a quote, start a quote
127
+ inQuote = true;
128
+ fieldQuoted = true;
129
+ } else {
130
+ // Next char is delimiter, this is an escaped delimiter
131
+ if (s.charAt(i + 1) === delimiter) {
132
+ field += delimiter;
133
+ // Skip the next char
134
+ i += 1;
135
+ } else {
136
+ // It's not escaping, so end quote
137
+ inQuote = false;
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ // Add the last field
145
+ field = processField(field);
146
+ row.push(field);
147
+ out.push(row);
148
+
149
+ return out;
150
+ };
151
+
152
+ var rxIsInt = /^\d+$/,
153
+ rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
154
+ // If a string has leading or trailing space,
155
+ // contains a comma double quote or a newline
156
+ // it needs to be quoted in CSV output
157
+ rxNeedsQuoting = /^\s|\s$|,|"|\n/,
158
+ trim = (function () {
159
+ // Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
160
+ if (String.prototype.trim) {
161
+ return function (s) {
162
+ return s.trim();
163
+ };
164
+ } else {
165
+ return function (s) {
166
+ return s.replace(/^\s*/, '').replace(/\s*$/, '');
167
+ };
168
+ }
169
+ }());
170
+
171
+ function chomp(s) {
172
+ if (s.charAt(s.length - 1) !== "\n") {
173
+ // Does not end with \n, just return string
174
+ return s;
175
+ } else {
176
+ // Remove the \n
177
+ return s.substring(0, s.length - 1);
178
+ }
179
+ }
180
+
181
+
182
+ }(this.recline.Backend.CSV));
@@ -0,0 +1,180 @@
1
+ this.recline = this.recline || {};
2
+ this.recline.Backend = this.recline.Backend || {};
3
+ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
4
+
5
+ (function($, my) {
6
+ my.__type__ = 'memory';
7
+
8
+ // ## Data Wrapper
9
+ //
10
+ // Turn a simple array of JS objects into a mini data-store with
11
+ // functionality like querying, faceting, updating (by ID) and deleting (by
12
+ // ID).
13
+ //
14
+ // @param data list of hashes for each record/row in the data ({key:
15
+ // value, key: value})
16
+ // @param fields (optional) list of field hashes (each hash defining a field
17
+ // as per recline.Model.Field). If fields not specified they will be taken
18
+ // from the data.
19
+ my.Store = function(data, fields) {
20
+ var self = this;
21
+ this.data = data;
22
+ if (fields) {
23
+ this.fields = fields;
24
+ } else {
25
+ if (data) {
26
+ this.fields = _.map(data[0], function(value, key) {
27
+ return {id: key};
28
+ });
29
+ }
30
+ }
31
+
32
+ this.update = function(doc) {
33
+ _.each(self.data, function(internalDoc, idx) {
34
+ if(doc.id === internalDoc.id) {
35
+ self.data[idx] = doc;
36
+ }
37
+ });
38
+ };
39
+
40
+ this.delete = function(doc) {
41
+ var newdocs = _.reject(self.data, function(internalDoc) {
42
+ return (doc.id === internalDoc.id);
43
+ });
44
+ this.data = newdocs;
45
+ };
46
+
47
+ this.save = function(changes, dataset) {
48
+ var self = this;
49
+ var dfd = $.Deferred();
50
+ // TODO _.each(changes.creates) { ... }
51
+ _.each(changes.updates, function(record) {
52
+ self.update(record);
53
+ });
54
+ _.each(changes.deletes, function(record) {
55
+ self.delete(record);
56
+ });
57
+ dfd.resolve();
58
+ return dfd.promise();
59
+ },
60
+
61
+ this.query = function(queryObj) {
62
+ var dfd = $.Deferred();
63
+ var numRows = queryObj.size || this.data.length;
64
+ var start = queryObj.from || 0;
65
+ var results = this.data;
66
+ results = this._applyFilters(results, queryObj);
67
+ results = this._applyFreeTextQuery(results, queryObj);
68
+ // not complete sorting!
69
+ _.each(queryObj.sort, function(sortObj) {
70
+ var fieldName = _.keys(sortObj)[0];
71
+ results = _.sortBy(results, function(doc) {
72
+ var _out = doc[fieldName];
73
+ return _out;
74
+ });
75
+ if (sortObj[fieldName].order == 'desc') {
76
+ results.reverse();
77
+ }
78
+ });
79
+ var facets = this.computeFacets(results, queryObj);
80
+ var out = {
81
+ total: results.length,
82
+ hits: results.slice(start, start+numRows),
83
+ facets: facets
84
+ };
85
+ dfd.resolve(out);
86
+ return dfd.promise();
87
+ };
88
+
89
+ // in place filtering
90
+ this._applyFilters = function(results, queryObj) {
91
+ _.each(queryObj.filters, function(filter) {
92
+ // if a term filter ...
93
+ if (filter.type === 'term') {
94
+ results = _.filter(results, function(doc) {
95
+ return (doc[filter.field] == filter.term);
96
+ });
97
+ }
98
+ });
99
+ return results;
100
+ };
101
+
102
+ // we OR across fields but AND across terms in query string
103
+ this._applyFreeTextQuery = function(results, queryObj) {
104
+ if (queryObj.q) {
105
+ var terms = queryObj.q.split(' ');
106
+ results = _.filter(results, function(rawdoc) {
107
+ var matches = true;
108
+ _.each(terms, function(term) {
109
+ var foundmatch = false;
110
+ _.each(self.fields, function(field) {
111
+ var value = rawdoc[field.id];
112
+ if (value !== null) {
113
+ value = value.toString();
114
+ } else {
115
+ // value can be null (apparently in some cases)
116
+ value = '';
117
+ }
118
+ // TODO regexes?
119
+ foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());
120
+ // TODO: early out (once we are true should break to spare unnecessary testing)
121
+ // if (foundmatch) return true;
122
+ });
123
+ matches = matches && foundmatch;
124
+ // TODO: early out (once false should break to spare unnecessary testing)
125
+ // if (!matches) return false;
126
+ });
127
+ return matches;
128
+ });
129
+ }
130
+ return results;
131
+ };
132
+
133
+ this.computeFacets = function(records, queryObj) {
134
+ var facetResults = {};
135
+ if (!queryObj.facets) {
136
+ return facetResults;
137
+ }
138
+ _.each(queryObj.facets, function(query, facetId) {
139
+ // TODO: remove dependency on recline.Model
140
+ facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
141
+ facetResults[facetId].termsall = {};
142
+ });
143
+ // faceting
144
+ _.each(records, function(doc) {
145
+ _.each(queryObj.facets, function(query, facetId) {
146
+ var fieldId = query.terms.field;
147
+ var val = doc[fieldId];
148
+ var tmp = facetResults[facetId];
149
+ if (val) {
150
+ tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
151
+ } else {
152
+ tmp.missing = tmp.missing + 1;
153
+ }
154
+ });
155
+ });
156
+ _.each(queryObj.facets, function(query, facetId) {
157
+ var tmp = facetResults[facetId];
158
+ var terms = _.map(tmp.termsall, function(count, term) {
159
+ return { term: term, count: count };
160
+ });
161
+ tmp.terms = _.sortBy(terms, function(item) {
162
+ // want descending order
163
+ return -item.count;
164
+ });
165
+ tmp.terms = tmp.terms.slice(0, 10);
166
+ });
167
+ return facetResults;
168
+ };
169
+
170
+ this.transform = function(editFunc) {
171
+ var toUpdate = costco.mapDocs(this.data, editFunc);
172
+ // TODO: very inefficient -- could probably just walk the documents and updates in tandem and update
173
+ _.each(toUpdate.updates, function(record, idx) {
174
+ self.data[idx] = record;
175
+ });
176
+ return this.save(toUpdate);
177
+ };
178
+ };
179
+
180
+ }(jQuery, this.recline.Backend.Memory));
@@ -0,0 +1,607 @@
1
+ // # Recline Backbone Models
2
+ this.recline = this.recline || {};
3
+ this.recline.Model = this.recline.Model || {};
4
+
5
+ (function($, my) {
6
+
7
+ // ## <a id="dataset">Dataset</a>
8
+ my.Dataset = Backbone.Model.extend({
9
+ constructor: function Dataset() {
10
+ Backbone.Model.prototype.constructor.apply(this, arguments);
11
+ },
12
+
13
+ // ### initialize
14
+ initialize: function() {
15
+ _.bindAll(this, 'query');
16
+ this.backend = null;
17
+ if (this.get('backend')) {
18
+ this.backend = this._backendFromString(this.get('backend'));
19
+ } else { // try to guess backend ...
20
+ if (this.get('records')) {
21
+ this.backend = recline.Backend.Memory;
22
+ }
23
+ }
24
+ this.fields = new my.FieldList();
25
+ this.records = new my.RecordList();
26
+ this._changes = {
27
+ deletes: [],
28
+ updates: [],
29
+ creates: []
30
+ };
31
+ this.facets = new my.FacetList();
32
+ this.recordCount = null;
33
+ this.queryState = new my.Query();
34
+ this.queryState.bind('change', this.query);
35
+ this.queryState.bind('facet:add', this.query);
36
+ // store is what we query and save against
37
+ // store will either be the backend or be a memory store if Backend fetch
38
+ // tells us to use memory store
39
+ this._store = this.backend;
40
+ if (this.backend == recline.Backend.Memory) {
41
+ this.fetch();
42
+ }
43
+ },
44
+
45
+ // ### fetch
46
+ //
47
+ // Retrieve dataset and (some) records from the backend.
48
+ fetch: function() {
49
+ var self = this;
50
+ var dfd = $.Deferred();
51
+
52
+ if (this.backend !== recline.Backend.Memory) {
53
+ this.backend.fetch(this.toJSON())
54
+ .done(handleResults)
55
+ .fail(function(arguments) {
56
+ dfd.reject(arguments);
57
+ });
58
+ } else {
59
+ // special case where we have been given data directly
60
+ handleResults({
61
+ records: this.get('records'),
62
+ fields: this.get('fields'),
63
+ useMemoryStore: true
64
+ });
65
+ }
66
+
67
+ function handleResults(results) {
68
+ var out = self._normalizeRecordsAndFields(results.records, results.fields);
69
+ if (results.useMemoryStore) {
70
+ self._store = new recline.Backend.Memory.Store(out.records, out.fields);
71
+ }
72
+
73
+ self.set(results.metadata);
74
+ self.fields.reset(out.fields);
75
+ self.query()
76
+ .done(function() {
77
+ dfd.resolve(self);
78
+ })
79
+ .fail(function(arguments) {
80
+ dfd.reject(arguments);
81
+ });
82
+ }
83
+
84
+ return dfd.promise();
85
+ },
86
+
87
+ // ### _normalizeRecordsAndFields
88
+ //
89
+ // 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
90
+ //
91
+ // e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] =>
92
+ // fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]
93
+ _normalizeRecordsAndFields: function(records, fields) {
94
+ // if no fields get them from records
95
+ if (!fields && records && records.length > 0) {
96
+ // records is array then fields is first row of records ...
97
+ if (records[0] instanceof Array) {
98
+ fields = records[0];
99
+ records = records.slice(1);
100
+ } else {
101
+ fields = _.map(_.keys(records[0]), function(key) {
102
+ return {id: key};
103
+ });
104
+ }
105
+ }
106
+
107
+ // fields is an array of strings (i.e. list of field headings/ids)
108
+ if (fields && fields.length > 0 && typeof fields[0] === 'string') {
109
+ // Rename duplicate fieldIds as each field name needs to be
110
+ // unique.
111
+ var seen = {};
112
+ fields = _.map(fields, function(field, index) {
113
+ // cannot use trim as not supported by IE7
114
+ var fieldId = field.replace(/^\s+|\s+$/g, '');
115
+ if (fieldId === '') {
116
+ fieldId = '_noname_';
117
+ field = fieldId;
118
+ }
119
+ while (fieldId in seen) {
120
+ seen[field] += 1;
121
+ fieldId = field + seen[field];
122
+ }
123
+ if (!(field in seen)) {
124
+ seen[field] = 0;
125
+ }
126
+ // TODO: decide whether to keep original name as label ...
127
+ // return { id: fieldId, label: field || fieldId }
128
+ return { id: fieldId };
129
+ });
130
+ }
131
+ // records is provided as arrays so need to zip together with fields
132
+ // NB: this requires you to have fields to match arrays
133
+ if (records && records.length > 0 && records[0] instanceof Array) {
134
+ records = _.map(records, function(doc) {
135
+ var tmp = {};
136
+ _.each(fields, function(field, idx) {
137
+ tmp[field.id] = doc[idx];
138
+ });
139
+ return tmp;
140
+ });
141
+ }
142
+ return {
143
+ fields: fields,
144
+ records: records
145
+ };
146
+ },
147
+
148
+ save: function() {
149
+ var self = this;
150
+ // TODO: need to reset the changes ...
151
+ return this._store.save(this._changes, this.toJSON());
152
+ },
153
+
154
+ transform: function(editFunc) {
155
+ var self = this;
156
+ if (!this._store.transform) {
157
+ alert('Transform is not supported with this backend: ' + this.get('backend'));
158
+ return;
159
+ }
160
+ this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
161
+ this._store.transform(editFunc).done(function() {
162
+ // reload data as records have changed
163
+ self.query();
164
+ self.trigger('recline:flash', {message: "Records updated successfully"});
165
+ });
166
+ },
167
+
168
+ // ### query
169
+ //
170
+ // AJAX method with promise API to get records from the backend.
171
+ //
172
+ // It will query based on current query state (given by this.queryState)
173
+ // updated by queryObj (if provided).
174
+ //
175
+ // Resulting RecordList are used to reset this.records and are
176
+ // also returned.
177
+ query: function(queryObj) {
178
+ var self = this;
179
+ var dfd = $.Deferred();
180
+ this.trigger('query:start');
181
+
182
+ if (queryObj) {
183
+ this.queryState.set(queryObj, {silent: true});
184
+ }
185
+ var actualQuery = this.queryState.toJSON();
186
+
187
+ this._store.query(actualQuery, this.toJSON())
188
+ .done(function(queryResult) {
189
+ self._handleQueryResult(queryResult);
190
+ self.trigger('query:done');
191
+ dfd.resolve(self.records);
192
+ })
193
+ .fail(function(arguments) {
194
+ self.trigger('query:fail', arguments);
195
+ dfd.reject(arguments);
196
+ });
197
+ return dfd.promise();
198
+ },
199
+
200
+ _handleQueryResult: function(queryResult) {
201
+ var self = this;
202
+ self.recordCount = queryResult.total;
203
+ var docs = _.map(queryResult.hits, function(hit) {
204
+ var _doc = new my.Record(hit);
205
+ _doc.fields = self.fields;
206
+ _doc.bind('change', function(doc) {
207
+ self._changes.updates.push(doc.toJSON());
208
+ });
209
+ _doc.bind('destroy', function(doc) {
210
+ self._changes.deletes.push(doc.toJSON());
211
+ });
212
+ return _doc;
213
+ });
214
+ self.records.reset(docs);
215
+ if (queryResult.facets) {
216
+ var facets = _.map(queryResult.facets, function(facetResult, facetId) {
217
+ facetResult.id = facetId;
218
+ return new my.Facet(facetResult);
219
+ });
220
+ self.facets.reset(facets);
221
+ }
222
+ },
223
+
224
+ toTemplateJSON: function() {
225
+ var data = this.toJSON();
226
+ data.recordCount = this.recordCount;
227
+ data.fields = this.fields.toJSON();
228
+ return data;
229
+ },
230
+
231
+ // ### getFieldsSummary
232
+ //
233
+ // Get a summary for each field in the form of a `Facet`.
234
+ //
235
+ // @return null as this is async function. Provides deferred/promise interface.
236
+ getFieldsSummary: function() {
237
+ var self = this;
238
+ var query = new my.Query();
239
+ query.set({size: 0});
240
+ this.fields.each(function(field) {
241
+ query.addFacet(field.id);
242
+ });
243
+ var dfd = $.Deferred();
244
+ this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
245
+ if (queryResult.facets) {
246
+ _.each(queryResult.facets, function(facetResult, facetId) {
247
+ facetResult.id = facetId;
248
+ var facet = new my.Facet(facetResult);
249
+ // TODO: probably want replace rather than reset (i.e. just replace the facet with this id)
250
+ self.fields.get(facetId).facets.reset(facet);
251
+ });
252
+ }
253
+ dfd.resolve(queryResult);
254
+ });
255
+ return dfd.promise();
256
+ },
257
+
258
+ // Deprecated (as of v0.5) - use record.summary()
259
+ recordSummary: function(record) {
260
+ return record.summary();
261
+ },
262
+
263
+ // ### _backendFromString(backendString)
264
+ //
265
+ // See backend argument to initialize for details
266
+ _backendFromString: function(backendString) {
267
+ var parts = backendString.split('.');
268
+ // walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
269
+ var current = window;
270
+ for(ii=0;ii<parts.length;ii++) {
271
+ if (!current) {
272
+ break;
273
+ }
274
+ current = current[parts[ii]];
275
+ }
276
+ if (current) {
277
+ return current;
278
+ }
279
+
280
+ // alternatively we just had a simple string
281
+ var backend = null;
282
+ if (recline && recline.Backend) {
283
+ _.each(_.keys(recline.Backend), function(name) {
284
+ if (name.toLowerCase() === backendString.toLowerCase()) {
285
+ backend = recline.Backend[name];
286
+ }
287
+ });
288
+ }
289
+ return backend;
290
+ }
291
+ });
292
+
293
+
294
+ // ### Dataset.restore
295
+ //
296
+ // Restore a Dataset instance from a serialized state. Serialized state for a
297
+ // Dataset is an Object like:
298
+ //
299
+ // <pre>
300
+ // {
301
+ // backend: {backend type - i.e. value of dataset.backend.__type__}
302
+ // dataset: {dataset info needed for loading -- result of dataset.toJSON() would be sufficient but can be simpler }
303
+ // // convenience - if url provided and dataste not this be used as dataset url
304
+ // url: {dataset url}
305
+ // ...
306
+ // }
307
+ my.Dataset.restore = function(state) {
308
+ var dataset = null;
309
+ // hack-y - restoring a memory dataset does not mean much ...
310
+ if (state.backend === 'memory') {
311
+ var datasetInfo = {
312
+ records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
313
+ };
314
+ } else {
315
+ var datasetInfo = {
316
+ url: state.url,
317
+ backend: state.backend
318
+ };
319
+ }
320
+ dataset = new recline.Model.Dataset(datasetInfo);
321
+ return dataset;
322
+ };
323
+
324
+ // ## <a id="record">A Record</a>
325
+ //
326
+ // A single record (or row) in the dataset
327
+ my.Record = Backbone.Model.extend({
328
+ constructor: function Record() {
329
+ Backbone.Model.prototype.constructor.apply(this, arguments);
330
+ },
331
+
332
+ // ### initialize
333
+ //
334
+ // Create a Record
335
+ //
336
+ // You usually will not do this directly but will have records created by
337
+ // Dataset e.g. in query method
338
+ //
339
+ // Certain methods require presence of a fields attribute (identical to that on Dataset)
340
+ initialize: function() {
341
+ _.bindAll(this, 'getFieldValue');
342
+ },
343
+
344
+ // ### getFieldValue
345
+ //
346
+ // For the provided Field get the corresponding rendered computed data value
347
+ // for this record.
348
+ getFieldValue: function(field) {
349
+ val = this.getFieldValueUnrendered(field);
350
+ if (field.renderer) {
351
+ val = field.renderer(val, field, this.toJSON());
352
+ }
353
+ return val;
354
+ },
355
+
356
+ // ### getFieldValueUnrendered
357
+ //
358
+ // For the provided Field get the corresponding computed data value
359
+ // for this record.
360
+ getFieldValueUnrendered: function(field) {
361
+ var val = this.get(field.id);
362
+ if (field.deriver) {
363
+ val = field.deriver(val, field, this);
364
+ }
365
+ return val;
366
+ },
367
+
368
+ // ### summary
369
+ //
370
+ // Get a simple html summary of this record in form of key/value list
371
+ summary: function(record) {
372
+ var self = this;
373
+ var html = '<div class="recline-record-summary">';
374
+ this.fields.each(function(field) {
375
+ if (field.id != 'id') {
376
+ html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + self.getFieldValue(field) + '</div>';
377
+ }
378
+ });
379
+ html += '</div>';
380
+ return html;
381
+ },
382
+
383
+ // Override Backbone save, fetch and destroy so they do nothing
384
+ // Instead, Dataset object that created this Record should take care of
385
+ // handling these changes (discovery will occur via event notifications)
386
+ // WARNING: these will not persist *unless* you call save on Dataset
387
+ fetch: function() {},
388
+ save: function() {},
389
+ destroy: function() { this.trigger('destroy', this); }
390
+ });
391
+
392
+
393
+ // ## A Backbone collection of Records
394
+ my.RecordList = Backbone.Collection.extend({
395
+ constructor: function RecordList() {
396
+ Backbone.Collection.prototype.constructor.apply(this, arguments);
397
+ },
398
+ model: my.Record
399
+ });
400
+
401
+
402
+ // ## <a id="field">A Field (aka Column) on a Dataset</a>
403
+ my.Field = Backbone.Model.extend({
404
+ constructor: function Field() {
405
+ Backbone.Model.prototype.constructor.apply(this, arguments);
406
+ },
407
+ // ### defaults - define default values
408
+ defaults: {
409
+ label: null,
410
+ type: 'string',
411
+ format: null,
412
+ is_derived: false
413
+ },
414
+ // ### initialize
415
+ //
416
+ // @param {Object} data: standard Backbone model attributes
417
+ //
418
+ // @param {Object} options: renderer and/or deriver functions.
419
+ initialize: function(data, options) {
420
+ // if a hash not passed in the first argument throw error
421
+ if ('0' in data) {
422
+ throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
423
+ }
424
+ if (this.attributes.label === null) {
425
+ this.set({label: this.id});
426
+ }
427
+ if (options) {
428
+ this.renderer = options.renderer;
429
+ this.deriver = options.deriver;
430
+ }
431
+ if (!this.renderer) {
432
+ this.renderer = this.defaultRenderers[this.get('type')];
433
+ }
434
+ this.facets = new my.FacetList();
435
+ },
436
+ defaultRenderers: {
437
+ object: function(val, field, doc) {
438
+ return JSON.stringify(val);
439
+ },
440
+ geo_point: function(val, field, doc) {
441
+ return JSON.stringify(val);
442
+ },
443
+ 'float': function(val, field, doc) {
444
+ var format = field.get('format');
445
+ if (format === 'percentage') {
446
+ return val + '%';
447
+ }
448
+ return val;
449
+ },
450
+ 'string': function(val, field, doc) {
451
+ var format = field.get('format');
452
+ if (format === 'markdown') {
453
+ if (typeof Showdown !== 'undefined') {
454
+ var showdown = new Showdown.converter();
455
+ out = showdown.makeHtml(val);
456
+ return out;
457
+ } else {
458
+ return val;
459
+ }
460
+ } else if (format == 'plain') {
461
+ return val;
462
+ } else {
463
+ // as this is the default and default type is string may get things
464
+ // here that are not actually strings
465
+ if (val && typeof val === 'string') {
466
+ val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
467
+ }
468
+ return val
469
+ }
470
+ }
471
+ }
472
+ });
473
+
474
+ my.FieldList = Backbone.Collection.extend({
475
+ constructor: function FieldList() {
476
+ Backbone.Collection.prototype.constructor.apply(this, arguments);
477
+ },
478
+ model: my.Field
479
+ });
480
+
481
+ // ## <a id="query">Query</a>
482
+ my.Query = Backbone.Model.extend({
483
+ constructor: function Query() {
484
+ Backbone.Model.prototype.constructor.apply(this, arguments);
485
+ },
486
+ defaults: function() {
487
+ return {
488
+ size: 100,
489
+ from: 0,
490
+ q: '',
491
+ facets: {},
492
+ filters: []
493
+ };
494
+ },
495
+ _filterTemplates: {
496
+ term: {
497
+ type: 'term',
498
+ field: '',
499
+ term: ''
500
+ },
501
+ geo_distance: {
502
+ distance: 10,
503
+ unit: 'km',
504
+ point: {
505
+ lon: 0,
506
+ lat: 0
507
+ }
508
+ }
509
+ },
510
+ // ### addFilter
511
+ //
512
+ // Add a new filter (appended to the list of filters)
513
+ //
514
+ // @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates
515
+ addFilter: function(filter) {
516
+ // crude deep copy
517
+ var ourfilter = JSON.parse(JSON.stringify(filter));
518
+ // not full specified so use template and over-write
519
+ if (_.keys(filter).length <= 2) {
520
+ ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
521
+ }
522
+ var filters = this.get('filters');
523
+ filters.push(ourfilter);
524
+ this.trigger('change:filters:new-blank');
525
+ },
526
+ updateFilter: function(index, value) {
527
+ },
528
+ // ### removeFilter
529
+ //
530
+ // Remove a filter from filters at index filterIndex
531
+ removeFilter: function(filterIndex) {
532
+ var filters = this.get('filters');
533
+ filters.splice(filterIndex, 1);
534
+ this.set({filters: filters});
535
+ this.trigger('change');
536
+ },
537
+ // ### addFacet
538
+ //
539
+ // Add a Facet to this query
540
+ //
541
+ // See <http://www.elasticsearch.org/guide/reference/api/search/facets/>
542
+ addFacet: function(fieldId) {
543
+ var facets = this.get('facets');
544
+ // 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)
545
+ if (_.contains(_.keys(facets), fieldId)) {
546
+ return;
547
+ }
548
+ facets[fieldId] = {
549
+ terms: { field: fieldId }
550
+ };
551
+ this.set({facets: facets}, {silent: true});
552
+ this.trigger('facet:add', this);
553
+ },
554
+ addHistogramFacet: function(fieldId) {
555
+ var facets = this.get('facets');
556
+ facets[fieldId] = {
557
+ date_histogram: {
558
+ field: fieldId,
559
+ interval: 'day'
560
+ }
561
+ };
562
+ this.set({facets: facets}, {silent: true});
563
+ this.trigger('facet:add', this);
564
+ }
565
+ });
566
+
567
+
568
+ // ## <a id="facet">A Facet (Result)</a>
569
+ my.Facet = Backbone.Model.extend({
570
+ constructor: function Facet() {
571
+ Backbone.Model.prototype.constructor.apply(this, arguments);
572
+ },
573
+ defaults: function() {
574
+ return {
575
+ _type: 'terms',
576
+ total: 0,
577
+ other: 0,
578
+ missing: 0,
579
+ terms: []
580
+ };
581
+ }
582
+ });
583
+
584
+ // ## A Collection/List of Facets
585
+ my.FacetList = Backbone.Collection.extend({
586
+ constructor: function FacetList() {
587
+ Backbone.Collection.prototype.constructor.apply(this, arguments);
588
+ },
589
+ model: my.Facet
590
+ });
591
+
592
+ // ## Object State
593
+ //
594
+ // Convenience Backbone model for storing (configuration) state of objects like Views.
595
+ my.ObjectState = Backbone.Model.extend({
596
+ });
597
+
598
+
599
+ // ## Backbone.sync
600
+ //
601
+ // Override Backbone.sync to hand off to sync function in relevant backend
602
+ Backbone.sync = function(method, model, options) {
603
+ return model.backend.sync(method, model, options);
604
+ };
605
+
606
+ }(jQuery, this.recline.Model));
607
+