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.
- data/lib/active_admin_csv_import.rb +2 -0
- data/lib/active_admin_csv_import/version.rb +1 -1
- data/vendor/assets/javascripts/backbone/backbone.js +1571 -0
- data/vendor/assets/javascripts/backbone/json2.js +486 -0
- data/vendor/assets/javascripts/backbone/underscore.js +1246 -0
- data/vendor/assets/javascripts/recline/backend.csv.js +182 -0
- data/vendor/assets/javascripts/recline/backend.memory.js +180 -0
- data/vendor/assets/javascripts/recline/model.js +607 -0
- data/vendor/assets/javascripts/underscore.string.min.js +1 -0
- metadata +15 -8
@@ -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
|
+
|