active_admin_csv_import 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|