PageRenderTime 64ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/ckan/public/scripts/vendor/recline/recline.js

https://bitbucket.org/philippkueng/ckan-liip
JavaScript | 4617 lines | 4146 code | 182 blank | 289 comment | 183 complexity | 66532048416b4a9eeffdf56331eef71e MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. this.recline = this.recline || {};
  2. this.recline.Backend = this.recline.Backend || {};
  3. this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
  4. (function($, my) {
  5. // ## CKAN Backend
  6. //
  7. // This provides connection to the CKAN DataStore (v2)
  8. //
  9. // General notes
  10. //
  11. // * Every dataset must have an id equal to its resource id on the CKAN instance
  12. // * You should set the CKAN API endpoint for requests by setting API_ENDPOINT value on this module (recline.Backend.Ckan.API_ENDPOINT)
  13. my.__type__ = 'ckan';
  14. // Default CKAN API endpoint used for requests (you can change this but it will affect every request!)
  15. my.API_ENDPOINT = 'http://datahub.io/api';
  16. // ### fetch
  17. my.fetch = function(dataset) {
  18. var wrapper = my.DataStore();
  19. var dfd = $.Deferred();
  20. var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0});
  21. jqxhr.done(function(results) {
  22. // map ckan types to our usual types ...
  23. var fields = _.map(results.result.fields, function(field) {
  24. field.type = field.type in CKAN_TYPES_MAP ? CKAN_TYPES_MAP[field.type] : field.type;
  25. return field;
  26. });
  27. var out = {
  28. fields: fields,
  29. useMemoryStore: false
  30. };
  31. dfd.resolve(out);
  32. });
  33. return dfd.promise();
  34. };
  35. // only put in the module namespace so we can access for tests!
  36. my._normalizeQuery = function(queryObj, dataset) {
  37. var actualQuery = {
  38. resource_id: dataset.id,
  39. q: queryObj.q,
  40. limit: queryObj.size || 10,
  41. offset: queryObj.from || 0
  42. };
  43. if (queryObj.sort && queryObj.sort.length > 0) {
  44. var _tmp = _.map(queryObj.sort, function(sortObj) {
  45. return sortObj.field + ' ' + (sortObj.order || '');
  46. });
  47. actualQuery.sort = _tmp.join(',');
  48. }
  49. return actualQuery;
  50. }
  51. my.query = function(queryObj, dataset) {
  52. var actualQuery = my._normalizeQuery(queryObj, dataset);
  53. var wrapper = my.DataStore();
  54. var dfd = $.Deferred();
  55. var jqxhr = wrapper.search(actualQuery);
  56. jqxhr.done(function(results) {
  57. var out = {
  58. total: results.result.total,
  59. hits: results.result.records,
  60. };
  61. dfd.resolve(out);
  62. });
  63. return dfd.promise();
  64. };
  65. // ### DataStore
  66. //
  67. // Simple wrapper around the CKAN DataStore API
  68. //
  69. // @param endpoint: CKAN api endpoint (e.g. http://datahub.io/api)
  70. my.DataStore = function(endpoint) {
  71. var that = {
  72. endpoint: endpoint || my.API_ENDPOINT
  73. };
  74. that.search = function(data) {
  75. var searchUrl = that.endpoint + '/3/action/datastore_search';
  76. var jqxhr = $.ajax({
  77. url: searchUrl,
  78. data: data,
  79. dataType: 'json'
  80. });
  81. return jqxhr;
  82. }
  83. return that;
  84. }
  85. var CKAN_TYPES_MAP = {
  86. 'int4': 'integer',
  87. 'int8': 'integer',
  88. 'float8': 'float',
  89. 'text': 'string',
  90. 'json': 'object',
  91. 'timestamp': 'date'
  92. };
  93. }(jQuery, this.recline.Backend.Ckan));
  94. this.recline = this.recline || {};
  95. this.recline.Backend = this.recline.Backend || {};
  96. this.recline.Backend.CSV = this.recline.Backend.CSV || {};
  97. (function(my) {
  98. // ## fetch
  99. //
  100. // 3 options
  101. //
  102. // 1. CSV local fileobject -> HTML5 file object + CSV parser
  103. // 2. Already have CSV string (in data) attribute -> CSV parser
  104. // 2. online CSV file that is ajax-able -> ajax + csv parser
  105. //
  106. // All options generates similar data and give a memory store outcome
  107. my.fetch = function(dataset) {
  108. var dfd = $.Deferred();
  109. if (dataset.file) {
  110. var reader = new FileReader();
  111. var encoding = dataset.encoding || 'UTF-8';
  112. reader.onload = function(e) {
  113. var rows = my.parseCSV(e.target.result, dataset);
  114. dfd.resolve({
  115. records: rows,
  116. metadata: {
  117. filename: dataset.file.name
  118. },
  119. useMemoryStore: true
  120. });
  121. };
  122. reader.onerror = function (e) {
  123. alert('Failed to load file. Code: ' + e.target.error.code);
  124. };
  125. reader.readAsText(dataset.file, encoding);
  126. } else if (dataset.data) {
  127. var rows = my.parseCSV(dataset.data, dataset);
  128. dfd.resolve({
  129. records: rows,
  130. useMemoryStore: true
  131. });
  132. } else if (dataset.url) {
  133. $.get(dataset.url).done(function(data) {
  134. var rows = my.parseCSV(data, dataset);
  135. dfd.resolve({
  136. records: rows,
  137. useMemoryStore: true
  138. });
  139. });
  140. }
  141. return dfd.promise();
  142. };
  143. // Converts a Comma Separated Values string into an array of arrays.
  144. // Each line in the CSV becomes an array.
  145. //
  146. // Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.
  147. //
  148. // @return The CSV parsed as an array
  149. // @type Array
  150. //
  151. // @param {String} s The string to convert
  152. // @param {Object} options Options for loading CSV including
  153. // @param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
  154. // @param {String} [separator=','] Separator for CSV file
  155. // Heavily based on uselesscode's JS CSV parser (MIT Licensed):
  156. // http://www.uselesscode.org/javascript/csv/
  157. my.parseCSV= function(s, options) {
  158. // Get rid of any trailing \n
  159. s = chomp(s);
  160. var options = options || {};
  161. var trm = (options.trim === false) ? false : true;
  162. var separator = options.separator || ',';
  163. var delimiter = options.delimiter || '"';
  164. var cur = '', // The character we are currently processing.
  165. inQuote = false,
  166. fieldQuoted = false,
  167. field = '', // Buffer for building up the current field
  168. row = [],
  169. out = [],
  170. i,
  171. processField;
  172. processField = function (field) {
  173. if (fieldQuoted !== true) {
  174. // If field is empty set to null
  175. if (field === '') {
  176. field = null;
  177. // If the field was not quoted and we are trimming fields, trim it
  178. } else if (trm === true) {
  179. field = trim(field);
  180. }
  181. // Convert unquoted numbers to their appropriate types
  182. if (rxIsInt.test(field)) {
  183. field = parseInt(field, 10);
  184. } else if (rxIsFloat.test(field)) {
  185. field = parseFloat(field, 10);
  186. }
  187. }
  188. return field;
  189. };
  190. for (i = 0; i < s.length; i += 1) {
  191. cur = s.charAt(i);
  192. // If we are at a EOF or EOR
  193. if (inQuote === false && (cur === separator || cur === "\n")) {
  194. field = processField(field);
  195. // Add the current field to the current row
  196. row.push(field);
  197. // If this is EOR append row to output and flush row
  198. if (cur === "\n") {
  199. out.push(row);
  200. row = [];
  201. }
  202. // Flush the field buffer
  203. field = '';
  204. fieldQuoted = false;
  205. } else {
  206. // If it's not a delimiter, add it to the field buffer
  207. if (cur !== delimiter) {
  208. field += cur;
  209. } else {
  210. if (!inQuote) {
  211. // We are not in a quote, start a quote
  212. inQuote = true;
  213. fieldQuoted = true;
  214. } else {
  215. // Next char is delimiter, this is an escaped delimiter
  216. if (s.charAt(i + 1) === delimiter) {
  217. field += delimiter;
  218. // Skip the next char
  219. i += 1;
  220. } else {
  221. // It's not escaping, so end quote
  222. inQuote = false;
  223. }
  224. }
  225. }
  226. }
  227. }
  228. // Add the last field
  229. field = processField(field);
  230. row.push(field);
  231. out.push(row);
  232. return out;
  233. };
  234. // Converts an array of arrays into a Comma Separated Values string.
  235. // Each array becomes a line in the CSV.
  236. //
  237. // Nulls are converted to empty fields and integers or floats are converted to non-quoted numbers.
  238. //
  239. // @return The array serialized as a CSV
  240. // @type String
  241. //
  242. // @param {Array} a The array of arrays to convert
  243. // @param {Object} options Options for loading CSV including
  244. // @param {String} [separator=','] Separator for CSV file
  245. // Heavily based on uselesscode's JS CSV parser (MIT Licensed):
  246. // http://www.uselesscode.org/javascript/csv/
  247. my.serializeCSV= function(a, options) {
  248. var options = options || {};
  249. var separator = options.separator || ',';
  250. var delimiter = options.delimiter || '"';
  251. var cur = '', // The character we are currently processing.
  252. field = '', // Buffer for building up the current field
  253. row = '',
  254. out = '',
  255. i,
  256. j,
  257. processField;
  258. processField = function (field) {
  259. if (field === null) {
  260. // If field is null set to empty string
  261. field = '';
  262. } else if (typeof field === "string" && rxNeedsQuoting.test(field)) {
  263. // Convert string to delimited string
  264. field = delimiter + field + delimiter;
  265. } else if (typeof field === "number") {
  266. // Convert number to string
  267. field = field.toString(10);
  268. }
  269. return field;
  270. };
  271. for (i = 0; i < a.length; i += 1) {
  272. cur = a[i];
  273. for (j = 0; j < cur.length; j += 1) {
  274. field = processField(cur[j]);
  275. // If this is EOR append row to output and flush row
  276. if (j === (cur.length - 1)) {
  277. row += field;
  278. out += row + "\n";
  279. row = '';
  280. } else {
  281. // Add the current field to the current row
  282. row += field + separator;
  283. }
  284. // Flush the field buffer
  285. field = '';
  286. }
  287. }
  288. return out;
  289. };
  290. var rxIsInt = /^\d+$/,
  291. rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
  292. // If a string has leading or trailing space,
  293. // contains a comma double quote or a newline
  294. // it needs to be quoted in CSV output
  295. rxNeedsQuoting = /^\s|\s$|,|"|\n/,
  296. trim = (function () {
  297. // Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
  298. if (String.prototype.trim) {
  299. return function (s) {
  300. return s.trim();
  301. };
  302. } else {
  303. return function (s) {
  304. return s.replace(/^\s*/, '').replace(/\s*$/, '');
  305. };
  306. }
  307. }());
  308. function chomp(s) {
  309. if (s.charAt(s.length - 1) !== "\n") {
  310. // Does not end with \n, just return string
  311. return s;
  312. } else {
  313. // Remove the \n
  314. return s.substring(0, s.length - 1);
  315. }
  316. }
  317. }(this.recline.Backend.CSV));
  318. this.recline = this.recline || {};
  319. this.recline.Backend = this.recline.Backend || {};
  320. this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
  321. (function($, my) {
  322. my.__type__ = 'dataproxy';
  323. // URL for the dataproxy
  324. my.dataproxy_url = 'http://jsonpdataproxy.appspot.com';
  325. // Timeout for dataproxy (after this time if no response we error)
  326. // Needed because use JSONP so do not receive e.g. 500 errors
  327. my.timeout = 5000;
  328. // ## load
  329. //
  330. // Load data from a URL via the [DataProxy](http://github.com/okfn/dataproxy).
  331. //
  332. // Returns array of field names and array of arrays for records
  333. my.fetch = function(dataset) {
  334. var data = {
  335. url: dataset.url,
  336. 'max-results': dataset.size || dataset.rows || 1000,
  337. type: dataset.format || ''
  338. };
  339. var jqxhr = $.ajax({
  340. url: my.dataproxy_url,
  341. data: data,
  342. dataType: 'jsonp'
  343. });
  344. var dfd = $.Deferred();
  345. _wrapInTimeout(jqxhr).done(function(results) {
  346. if (results.error) {
  347. dfd.reject(results.error);
  348. }
  349. dfd.resolve({
  350. records: results.data,
  351. fields: results.fields,
  352. useMemoryStore: true
  353. });
  354. })
  355. .fail(function(arguments) {
  356. dfd.reject(arguments);
  357. });
  358. return dfd.promise();
  359. };
  360. // ## _wrapInTimeout
  361. //
  362. // Convenience method providing a crude way to catch backend errors on JSONP calls.
  363. // Many of backends use JSONP and so will not get error messages and this is
  364. // a crude way to catch those errors.
  365. var _wrapInTimeout = function(ourFunction) {
  366. var dfd = $.Deferred();
  367. var timer = setTimeout(function() {
  368. dfd.reject({
  369. message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
  370. });
  371. }, my.timeout);
  372. ourFunction.done(function(arguments) {
  373. clearTimeout(timer);
  374. dfd.resolve(arguments);
  375. })
  376. .fail(function(arguments) {
  377. clearTimeout(timer);
  378. dfd.reject(arguments);
  379. })
  380. ;
  381. return dfd.promise();
  382. }
  383. }(jQuery, this.recline.Backend.DataProxy));
  384. this.recline = this.recline || {};
  385. this.recline.Backend = this.recline.Backend || {};
  386. this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
  387. (function($, my) {
  388. my.__type__ = 'elasticsearch';
  389. // ## ElasticSearch Wrapper
  390. //
  391. // A simple JS wrapper around an [ElasticSearch](http://www.elasticsearch.org/) endpoints.
  392. //
  393. // @param {String} endpoint: url for ElasticSearch type/table, e.g. for ES running
  394. // on http://localhost:9200 with index twitter and type tweet it would be:
  395. //
  396. // <pre>http://localhost:9200/twitter/tweet</pre>
  397. //
  398. // @param {Object} options: set of options such as:
  399. //
  400. // * headers - {dict of headers to add to each request}
  401. // * dataType: dataType for AJAx requests e.g. set to jsonp to make jsonp requests (default is json requests)
  402. my.Wrapper = function(endpoint, options) {
  403. var self = this;
  404. this.endpoint = endpoint;
  405. this.options = _.extend({
  406. dataType: 'json'
  407. },
  408. options);
  409. // ### mapping
  410. //
  411. // Get ES mapping for this type/table
  412. //
  413. // @return promise compatible deferred object.
  414. this.mapping = function() {
  415. var schemaUrl = self.endpoint + '/_mapping';
  416. var jqxhr = makeRequest({
  417. url: schemaUrl,
  418. dataType: this.options.dataType
  419. });
  420. return jqxhr;
  421. };
  422. // ### get
  423. //
  424. // Get record corresponding to specified id
  425. //
  426. // @return promise compatible deferred object.
  427. this.get = function(id) {
  428. var base = this.endpoint + '/' + id;
  429. return makeRequest({
  430. url: base,
  431. dataType: 'json'
  432. });
  433. };
  434. // ### upsert
  435. //
  436. // create / update a record to ElasticSearch backend
  437. //
  438. // @param {Object} doc an object to insert to the index.
  439. // @return deferred supporting promise API
  440. this.upsert = function(doc) {
  441. var data = JSON.stringify(doc);
  442. url = this.endpoint;
  443. if (doc.id) {
  444. url += '/' + doc.id;
  445. }
  446. return makeRequest({
  447. url: url,
  448. type: 'POST',
  449. data: data,
  450. dataType: 'json'
  451. });
  452. };
  453. // ### delete
  454. //
  455. // Delete a record from the ElasticSearch backend.
  456. //
  457. // @param {Object} id id of object to delete
  458. // @return deferred supporting promise API
  459. this.delete = function(id) {
  460. url = this.endpoint;
  461. url += '/' + id;
  462. return makeRequest({
  463. url: url,
  464. type: 'DELETE',
  465. dataType: 'json'
  466. });
  467. };
  468. this._normalizeQuery = function(queryObj) {
  469. var self = this;
  470. var queryInfo = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
  471. var out = {
  472. constant_score: {
  473. query: {}
  474. }
  475. };
  476. if (!queryInfo.q) {
  477. out.constant_score.query = {
  478. match_all: {}
  479. };
  480. } else {
  481. out.constant_score.query = {
  482. query_string: {
  483. query: queryInfo.q
  484. }
  485. };
  486. }
  487. if (queryInfo.filters && queryInfo.filters.length) {
  488. out.constant_score.filter = {
  489. and: []
  490. };
  491. _.each(queryInfo.filters, function(filter) {
  492. out.constant_score.filter.and.push(self._convertFilter(filter));
  493. });
  494. }
  495. return out;
  496. },
  497. // convert from Recline sort structure to ES form
  498. // http://www.elasticsearch.org/guide/reference/api/search/sort.html
  499. this._normalizeSort = function(sort) {
  500. var out = _.map(sort, function(sortObj) {
  501. var _tmp = {};
  502. var _tmp2 = _.clone(sortObj);
  503. delete _tmp2['field'];
  504. _tmp[sortObj.field] = _tmp2;
  505. return _tmp;
  506. });
  507. return out;
  508. },
  509. this._convertFilter = function(filter) {
  510. var out = {};
  511. out[filter.type] = {}
  512. if (filter.type === 'term') {
  513. out.term[filter.field] = filter.term.toLowerCase();
  514. } else if (filter.type === 'geo_distance') {
  515. out.geo_distance[filter.field] = filter.point;
  516. out.geo_distance.distance = filter.distance;
  517. out.geo_distance.unit = filter.unit;
  518. }
  519. return out;
  520. },
  521. // ### query
  522. //
  523. // @return deferred supporting promise API
  524. this.query = function(queryObj) {
  525. var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
  526. esQuery.query = this._normalizeQuery(queryObj);
  527. delete esQuery.q;
  528. delete esQuery.filters;
  529. if (esQuery.sort && esQuery.sort.length > 0) {
  530. esQuery.sort = this._normalizeSort(esQuery.sort);
  531. }
  532. var data = {source: JSON.stringify(esQuery)};
  533. var url = this.endpoint + '/_search';
  534. var jqxhr = makeRequest({
  535. url: url,
  536. data: data,
  537. dataType: this.options.dataType
  538. });
  539. return jqxhr;
  540. }
  541. };
  542. // ## Recline Connectors
  543. //
  544. // Requires URL of ElasticSearch endpoint to be specified on the dataset
  545. // via the url attribute.
  546. // ES options which are passed through to `options` on Wrapper (see Wrapper for details)
  547. my.esOptions = {};
  548. // ### fetch
  549. my.fetch = function(dataset) {
  550. var es = new my.Wrapper(dataset.url, my.esOptions);
  551. var dfd = $.Deferred();
  552. es.mapping().done(function(schema) {
  553. if (!schema){
  554. dfd.reject({'message':'Elastic Search did not return a mapping'});
  555. return;
  556. }
  557. // only one top level key in ES = the type so we can ignore it
  558. var key = _.keys(schema)[0];
  559. var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
  560. dict.id = fieldName;
  561. return dict;
  562. });
  563. dfd.resolve({
  564. fields: fieldData
  565. });
  566. })
  567. .fail(function(arguments) {
  568. dfd.reject(arguments);
  569. });
  570. return dfd.promise();
  571. };
  572. // ### save
  573. my.save = function(changes, dataset) {
  574. var es = new my.Wrapper(dataset.url, my.esOptions);
  575. if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) {
  576. var dfd = $.Deferred();
  577. msg = 'Saving more than one item at a time not yet supported';
  578. alert(msg);
  579. dfd.reject(msg);
  580. return dfd.promise();
  581. }
  582. if (changes.creates.length > 0) {
  583. return es.upsert(changes.creates[0]);
  584. }
  585. else if (changes.updates.length >0) {
  586. return es.upsert(changes.updates[0]);
  587. } else if (changes.deletes.length > 0) {
  588. return es.delete(changes.deletes[0].id);
  589. }
  590. };
  591. // ### query
  592. my.query = function(queryObj, dataset) {
  593. var dfd = $.Deferred();
  594. var es = new my.Wrapper(dataset.url, my.esOptions);
  595. var jqxhr = es.query(queryObj);
  596. jqxhr.done(function(results) {
  597. var out = {
  598. total: results.hits.total,
  599. };
  600. out.hits = _.map(results.hits.hits, function(hit) {
  601. if (!('id' in hit._source) && hit._id) {
  602. hit._source.id = hit._id;
  603. }
  604. return hit._source;
  605. });
  606. if (results.facets) {
  607. out.facets = results.facets;
  608. }
  609. dfd.resolve(out);
  610. }).fail(function(errorObj) {
  611. var out = {
  612. title: 'Failed: ' + errorObj.status + ' code',
  613. message: errorObj.responseText
  614. };
  615. dfd.reject(out);
  616. });
  617. return dfd.promise();
  618. };
  619. // ### makeRequest
  620. //
  621. // Just $.ajax but in any headers in the 'headers' attribute of this
  622. // Backend instance. Example:
  623. //
  624. // <pre>
  625. // var jqxhr = this._makeRequest({
  626. // url: the-url
  627. // });
  628. // </pre>
  629. var makeRequest = function(data, headers) {
  630. var extras = {};
  631. if (headers) {
  632. extras = {
  633. beforeSend: function(req) {
  634. _.each(headers, function(value, key) {
  635. req.setRequestHeader(key, value);
  636. });
  637. }
  638. };
  639. }
  640. var data = _.extend(extras, data);
  641. return $.ajax(data);
  642. };
  643. }(jQuery, this.recline.Backend.ElasticSearch));
  644. this.recline = this.recline || {};
  645. this.recline.Backend = this.recline.Backend || {};
  646. this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
  647. (function($, my) {
  648. my.__type__ = 'gdocs';
  649. // ## Google spreadsheet backend
  650. //
  651. // Fetch data from a Google Docs spreadsheet.
  652. //
  653. // Dataset must have a url attribute pointing to the Gdocs or its JSON feed e.g.
  654. // <pre>
  655. // var dataset = new recline.Model.Dataset({
  656. // url: 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdGlQVDJnbjZRSU1tUUJWOUZXRG53VkE#gid=0'
  657. // },
  658. // 'gdocs'
  659. // );
  660. //
  661. // var dataset = new recline.Model.Dataset({
  662. // url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
  663. // },
  664. // 'gdocs'
  665. // );
  666. // </pre>
  667. //
  668. // @return object with two attributes
  669. //
  670. // * fields: array of Field objects
  671. // * records: array of objects for each row
  672. my.fetch = function(dataset) {
  673. var dfd = $.Deferred();
  674. var urls = my.getGDocsAPIUrls(dataset.url);
  675. // TODO cover it with tests
  676. // get the spreadsheet title
  677. (function () {
  678. var titleDfd = $.Deferred();
  679. $.getJSON(urls.spreadsheet, function (d) {
  680. titleDfd.resolve({
  681. spreadsheetTitle: d.feed.title.$t
  682. });
  683. });
  684. return titleDfd.promise();
  685. }()).then(function (response) {
  686. // get the actual worksheet data
  687. $.getJSON(urls.worksheet, function(d) {
  688. var result = my.parseData(d);
  689. var fields = _.map(result.fields, function(fieldId) {
  690. return {id: fieldId};
  691. });
  692. dfd.resolve({
  693. metadata: {
  694. title: response.spreadsheetTitle +" :: "+ result.worksheetTitle,
  695. spreadsheetTitle: response.spreadsheetTitle,
  696. worksheetTitle : result.worksheetTitle
  697. },
  698. records : result.records,
  699. fields : fields,
  700. useMemoryStore: true
  701. });
  702. });
  703. });
  704. return dfd.promise();
  705. };
  706. // ## parseData
  707. //
  708. // Parse data from Google Docs API into a reasonable form
  709. //
  710. // :options: (optional) optional argument dictionary:
  711. // columnsToUse: list of columns to use (specified by field names)
  712. // colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
  713. // :return: tabular data object (hash with keys: field and data).
  714. //
  715. // Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
  716. my.parseData = function(gdocsSpreadsheet, options) {
  717. var options = options || {};
  718. var colTypes = options.colTypes || {};
  719. var results = {
  720. fields : [],
  721. records: []
  722. };
  723. var entries = gdocsSpreadsheet.feed.entry || [];
  724. var key;
  725. var colName;
  726. // percentage values (e.g. 23.3%)
  727. var rep = /^([\d\.\-]+)\%$/;
  728. for(key in entries[0]) {
  729. // it's barely possible it has inherited keys starting with 'gsx$'
  730. if(/^gsx/.test(key)) {
  731. colName = key.substr(4);
  732. results.fields.push(colName);
  733. }
  734. }
  735. // converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])
  736. results.records = _.map(entries, function(entry) {
  737. var row = {};
  738. _.each(results.fields, function(col) {
  739. var _keyname = 'gsx$' + col;
  740. var value = entry[_keyname].$t;
  741. var num;
  742. // TODO cover this part of code with test
  743. // TODO use the regexp only once
  744. // if labelled as % and value contains %, convert
  745. if(colTypes[col] === 'percent' && rep.test(value)) {
  746. num = rep.exec(value)[1];
  747. value = parseFloat(num) / 100;
  748. }
  749. row[col] = value;
  750. });
  751. return row;
  752. });
  753. results.worksheetTitle = gdocsSpreadsheet.feed.title.$t;
  754. return results;
  755. };
  756. // Convenience function to get GDocs JSON API Url from standard URL
  757. my.getGDocsAPIUrls = function(url) {
  758. // https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY
  759. var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*gid=([\d]+).*/;
  760. var matches = url.match(regex);
  761. var key;
  762. var worksheet;
  763. var urls;
  764. if(!!matches) {
  765. key = matches[1];
  766. // the gid in url is 0-based and feed url is 1-based
  767. worksheet = parseInt(matches[2]) + 1;
  768. urls = {
  769. worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
  770. spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
  771. }
  772. }
  773. else {
  774. // we assume that it's one of the feeds urls
  775. key = url.split('/')[5];
  776. // by default then, take first worksheet
  777. worksheet = 1;
  778. urls = {
  779. worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
  780. spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
  781. }
  782. }
  783. return urls;
  784. };
  785. }(jQuery, this.recline.Backend.GDocs));
  786. this.recline = this.recline || {};
  787. this.recline.Backend = this.recline.Backend || {};
  788. this.recline.Backend.Memory = this.recline.Backend.Memory || {};
  789. (function($, my) {
  790. my.__type__ = 'memory';
  791. // ## Data Wrapper
  792. //
  793. // Turn a simple array of JS objects into a mini data-store with
  794. // functionality like querying, faceting, updating (by ID) and deleting (by
  795. // ID).
  796. //
  797. // @param data list of hashes for each record/row in the data ({key:
  798. // value, key: value})
  799. // @param fields (optional) list of field hashes (each hash defining a field
  800. // as per recline.Model.Field). If fields not specified they will be taken
  801. // from the data.
  802. my.Store = function(data, fields) {
  803. var self = this;
  804. this.data = data;
  805. if (fields) {
  806. this.fields = fields;
  807. } else {
  808. if (data) {
  809. this.fields = _.map(data[0], function(value, key) {
  810. return {id: key};
  811. });
  812. }
  813. }
  814. this.update = function(doc) {
  815. _.each(self.data, function(internalDoc, idx) {
  816. if(doc.id === internalDoc.id) {
  817. self.data[idx] = doc;
  818. }
  819. });
  820. };
  821. this.delete = function(doc) {
  822. var newdocs = _.reject(self.data, function(internalDoc) {
  823. return (doc.id === internalDoc.id);
  824. });
  825. this.data = newdocs;
  826. };
  827. this.save = function(changes, dataset) {
  828. var self = this;
  829. var dfd = $.Deferred();
  830. // TODO _.each(changes.creates) { ... }
  831. _.each(changes.updates, function(record) {
  832. self.update(record);
  833. });
  834. _.each(changes.deletes, function(record) {
  835. self.delete(record);
  836. });
  837. dfd.resolve();
  838. return dfd.promise();
  839. },
  840. this.query = function(queryObj) {
  841. var dfd = $.Deferred();
  842. var numRows = queryObj.size || this.data.length;
  843. var start = queryObj.from || 0;
  844. var results = this.data;
  845. results = this._applyFilters(results, queryObj);
  846. results = this._applyFreeTextQuery(results, queryObj);
  847. // TODO: this is not complete sorting!
  848. // What's wrong is we sort on the *last* entry in the sort list if there are multiple sort criteria
  849. _.each(queryObj.sort, function(sortObj) {
  850. var fieldName = sortObj.field;
  851. results = _.sortBy(results, function(doc) {
  852. var _out = doc[fieldName];
  853. return _out;
  854. });
  855. if (sortObj.order == 'desc') {
  856. results.reverse();
  857. }
  858. });
  859. var facets = this.computeFacets(results, queryObj);
  860. var out = {
  861. total: results.length,
  862. hits: results.slice(start, start+numRows),
  863. facets: facets
  864. };
  865. dfd.resolve(out);
  866. return dfd.promise();
  867. };
  868. // in place filtering
  869. this._applyFilters = function(results, queryObj) {
  870. var filters = queryObj.filters;
  871. // register filters
  872. var filterFunctions = {
  873. term : term,
  874. range : range,
  875. geo_distance : geo_distance
  876. };
  877. var dataParsers = {
  878. number : function (e) { return parseFloat(e, 10); },
  879. string : function (e) { return e.toString() },
  880. date : function (e) { return new Date(e).valueOf() }
  881. };
  882. // filter records
  883. return _.filter(results, function (record) {
  884. var passes = _.map(filters, function (filter) {
  885. return filterFunctions[filter.type](record, filter);
  886. });
  887. // return only these records that pass all filters
  888. return _.all(passes, _.identity);
  889. });
  890. // filters definitions
  891. function term(record, filter) {
  892. var parse = dataParsers[filter.fieldType];
  893. var value = parse(record[filter.field]);
  894. var term = parse(filter.term);
  895. return (value === term);
  896. }
  897. function range(record, filter) {
  898. var parse = dataParsers[filter.fieldType];
  899. var value = parse(record[filter.field]);
  900. var start = parse(filter.start);
  901. var stop = parse(filter.stop);
  902. return (value >= start && value <= stop);
  903. }
  904. function geo_distance() {
  905. // TODO code here
  906. }
  907. };
  908. // we OR across fields but AND across terms in query string
  909. this._applyFreeTextQuery = function(results, queryObj) {
  910. if (queryObj.q) {
  911. var terms = queryObj.q.split(' ');
  912. results = _.filter(results, function(rawdoc) {
  913. var matches = true;
  914. _.each(terms, function(term) {
  915. var foundmatch = false;
  916. _.each(self.fields, function(field) {
  917. var value = rawdoc[field.id];
  918. if (value !== null) {
  919. value = value.toString();
  920. } else {
  921. // value can be null (apparently in some cases)
  922. value = '';
  923. }
  924. // TODO regexes?
  925. foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());
  926. // TODO: early out (once we are true should break to spare unnecessary testing)
  927. // if (foundmatch) return true;
  928. });
  929. matches = matches && foundmatch;
  930. // TODO: early out (once false should break to spare unnecessary testing)
  931. // if (!matches) return false;
  932. });
  933. return matches;
  934. });
  935. }
  936. return results;
  937. };
  938. this.computeFacets = function(records, queryObj) {
  939. var facetResults = {};
  940. if (!queryObj.facets) {
  941. return facetResults;
  942. }
  943. _.each(queryObj.facets, function(query, facetId) {
  944. // TODO: remove dependency on recline.Model
  945. facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
  946. facetResults[facetId].termsall = {};
  947. });
  948. // faceting
  949. _.each(records, function(doc) {
  950. _.each(queryObj.facets, function(query, facetId) {
  951. var fieldId = query.terms.field;
  952. var val = doc[fieldId];
  953. var tmp = facetResults[facetId];
  954. if (val) {
  955. tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
  956. } else {
  957. tmp.missing = tmp.missing + 1;
  958. }
  959. });
  960. });
  961. _.each(queryObj.facets, function(query, facetId) {
  962. var tmp = facetResults[facetId];
  963. var terms = _.map(tmp.termsall, function(count, term) {
  964. return { term: term, count: count };
  965. });
  966. tmp.terms = _.sortBy(terms, function(item) {
  967. // want descending order
  968. return -item.count;
  969. });
  970. tmp.terms = tmp.terms.slice(0, 10);
  971. });
  972. return facetResults;
  973. };
  974. this.transform = function(editFunc) {
  975. var toUpdate = recline.Data.Transform.mapDocs(this.data, editFunc);
  976. // TODO: very inefficient -- could probably just walk the documents and updates in tandem and update
  977. _.each(toUpdate.updates, function(record, idx) {
  978. self.data[idx] = record;
  979. });
  980. return this.save(toUpdate);
  981. };
  982. };
  983. }(jQuery, this.recline.Backend.Memory));
  984. this.recline = this.recline || {};
  985. this.recline.Data = this.recline.Data || {};
  986. (function(my) {
  987. // adapted from https://github.com/harthur/costco. heather rules
  988. my.Transform = {};
  989. my.Transform.evalFunction = function(funcString) {
  990. try {
  991. eval("var editFunc = " + funcString);
  992. } catch(e) {
  993. return {errorMessage: e+""};
  994. }
  995. return editFunc;
  996. };
  997. my.Transform.previewTransform = function(docs, editFunc, currentColumn) {
  998. var preview = [];
  999. var updated = my.Transform.mapDocs($.extend(true, {}, docs), editFunc);
  1000. for (var i = 0; i < updated.docs.length; i++) {
  1001. var before = docs[i]
  1002. , after = updated.docs[i]
  1003. ;
  1004. if (!after) after = {};
  1005. if (currentColumn) {
  1006. preview.push({before: before[currentColumn], after: after[currentColumn]});
  1007. } else {
  1008. preview.push({before: before, after: after});
  1009. }
  1010. }
  1011. return preview;
  1012. };
  1013. my.Transform.mapDocs = function(docs, editFunc) {
  1014. var edited = []
  1015. , deleted = []
  1016. , failed = []
  1017. ;
  1018. var updatedDocs = _.map(docs, function(doc) {
  1019. try {
  1020. var updated = editFunc(_.clone(doc));
  1021. } catch(e) {
  1022. failed.push(doc);
  1023. return;
  1024. }
  1025. if(updated === null) {
  1026. updated = {_deleted: true};
  1027. edited.push(updated);
  1028. deleted.push(doc);
  1029. }
  1030. else if(updated && !_.isEqual(updated, doc)) {
  1031. edited.push(updated);
  1032. }
  1033. return updated;
  1034. });
  1035. return {
  1036. updates: edited,
  1037. docs: updatedDocs,
  1038. deletes: deleted,
  1039. failed: failed
  1040. };
  1041. };
  1042. }(this.recline.Data))
  1043. // # Recline Backbone Models
  1044. this.recline = this.recline || {};
  1045. this.recline.Model = this.recline.Model || {};
  1046. (function($, my) {
  1047. // ## <a id="dataset">Dataset</a>
  1048. my.Dataset = Backbone.Model.extend({
  1049. constructor: function Dataset() {
  1050. Backbone.Model.prototype.constructor.apply(this, arguments);
  1051. },
  1052. // ### initialize
  1053. initialize: function() {
  1054. _.bindAll(this, 'query');
  1055. this.backend = null;
  1056. if (this.get('backend')) {
  1057. this.backend = this._backendFromString(this.get('backend'));
  1058. } else { // try to guess backend ...
  1059. if (this.get('records')) {
  1060. this.backend = recline.Backend.Memory;
  1061. }
  1062. }
  1063. this.fields = new my.FieldList();
  1064. this.records = new my.RecordList();
  1065. this._changes = {
  1066. deletes: [],
  1067. updates: [],
  1068. creates: []
  1069. };
  1070. this.facets = new my.FacetList();
  1071. this.recordCount = null;
  1072. this.queryState = new my.Query();
  1073. this.queryState.bind('change', this.query);
  1074. this.queryState.bind('facet:add', this.query);
  1075. // store is what we query and save against
  1076. // store will either be the backend or be a memory store if Backend fetch
  1077. // tells us to use memory store
  1078. this._store = this.backend;
  1079. if (this.backend == recline.Backend.Memory) {
  1080. this.fetch();
  1081. }
  1082. },
  1083. // ### fetch
  1084. //
  1085. // Retrieve dataset and (some) records from the backend.
  1086. fetch: function() {
  1087. var self = this;
  1088. var dfd = $.Deferred();
  1089. if (this.backend !== recline.Backend.Memory) {
  1090. this.backend.fetch(this.toJSON())
  1091. .done(handleResults)
  1092. .fail(function(arguments) {
  1093. dfd.reject(arguments);
  1094. });
  1095. } else {
  1096. // special case where we have been given data directly
  1097. handleResults({
  1098. records: this.get('records'),
  1099. fields: this.get('fields'),
  1100. useMemoryStore: true
  1101. });
  1102. }
  1103. function handleResults(results) {
  1104. var out = self._normalizeRecordsAndFields(results.records, results.fields);
  1105. if (results.useMemoryStore) {
  1106. self._store = new recline.Backend.Memory.Store(out.records, out.fields);
  1107. }
  1108. self.set(results.metadata);
  1109. self.fields.reset(out.fields);
  1110. self.query()
  1111. .done(function() {
  1112. dfd.resolve(self);
  1113. })
  1114. .fail(function(arguments) {
  1115. dfd.reject(arguments);
  1116. });
  1117. }
  1118. return dfd.promise();
  1119. },
  1120. // ### _normalizeRecordsAndFields
  1121. //
  1122. // 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
  1123. //
  1124. // e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] =>
  1125. // fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]
  1126. _normalizeRecordsAndFields: function(records, fields) {
  1127. // if no fields get them from records
  1128. if (!fields && records && records.length > 0) {
  1129. // records is array then fields is first row of records ...
  1130. if (records[0] instanceof Array) {
  1131. fields = records[0];
  1132. records = records.slice(1);
  1133. } else {
  1134. fields = _.map(_.keys(records[0]), function(key) {
  1135. return {id: key};
  1136. });
  1137. }
  1138. }
  1139. // fields is an array of strings (i.e. list of field headings/ids)
  1140. if (fields && fields.length > 0 && typeof fields[0] === 'string') {
  1141. // Rename duplicate fieldIds as each field name needs to be
  1142. // unique.
  1143. var seen = {};
  1144. fields = _.map(fields, function(field, index) {
  1145. // cannot use trim as not supported by IE7
  1146. var fieldId = field.replace(/^\s+|\s+$/g, '');
  1147. if (fieldId === '') {
  1148. fieldId = '_noname_';
  1149. field = fieldId;
  1150. }
  1151. while (fieldId in seen) {
  1152. seen[field] += 1;
  1153. fieldId = field + seen[field];
  1154. }
  1155. if (!(field in seen)) {
  1156. seen[field] = 0;
  1157. }
  1158. // TODO: decide whether to keep original name as label ...
  1159. // return { id: fieldId, label: field || fieldId }
  1160. return { id: fieldId };
  1161. });
  1162. }
  1163. // records is provided as arrays so need to zip together with fields
  1164. // NB: this requires you to have fields to match arrays
  1165. if (records && records.length > 0 && records[0] instanceof Array) {
  1166. records = _.map(records, function(doc) {
  1167. var tmp = {};
  1168. _.each(fields, function(field, idx) {
  1169. tmp[field.id] = doc[idx];
  1170. });
  1171. return tmp;
  1172. });
  1173. }
  1174. return {
  1175. fields: fields,
  1176. records: records
  1177. };
  1178. },
  1179. save: function() {
  1180. var self = this;
  1181. // TODO: need to reset the changes ...
  1182. return this._store.save(this._changes, this.toJSON());
  1183. },
  1184. transform: function(editFunc) {
  1185. var self = this;
  1186. if (!this._store.transform) {
  1187. alert('Transform is not supported with this backend: ' + this.get('backend'));
  1188. return;
  1189. }
  1190. this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
  1191. this._store.transform(editFunc).done(function() {
  1192. // reload data as records have changed
  1193. self.query();
  1194. self.trigger('recline:flash', {message: "Records updated successfully"});
  1195. });
  1196. },
  1197. // ### query
  1198. //
  1199. // AJAX method with promise API to get records from the backend.
  1200. //
  1201. // It will query based on current query state (given by this.queryState)
  1202. // updated by queryObj (if provided).
  1203. //
  1204. // Resulting RecordList are used to reset this.records and are
  1205. // also returned.
  1206. query: function(queryObj) {
  1207. var self = this;
  1208. var dfd = $.Deferred();
  1209. this.trigger('query:start');
  1210. if (queryObj) {
  1211. this.queryState.set(queryObj, {silent: true});
  1212. }
  1213. var actualQuery = this.queryState.toJSON();
  1214. this._store.query(actualQuery, this.toJSON())
  1215. .done(function(queryResult) {
  1216. self._handleQueryResult(queryResult);
  1217. self.trigger('query:done');
  1218. dfd.resolve(self.records);
  1219. })
  1220. .fail(function(arguments) {
  1221. self.trigger('query:fail', arguments);
  1222. dfd.reject(arguments);
  1223. });
  1224. return dfd.promise();
  1225. },
  1226. _handleQueryResult: function(queryResult) {
  1227. var self = this;
  1228. self.recordCount = queryResult.total;
  1229. var docs = _.map(queryResult.hits, function(hit) {
  1230. var _doc = new my.Record(hit);
  1231. _doc.fields = self.fields;
  1232. _doc.bind('change', function(doc) {
  1233. self._changes.updates.push(doc.toJSON());
  1234. });
  1235. _doc.bind('destroy', function(doc) {
  1236. self._changes.deletes.push(doc.toJSON());
  1237. });
  1238. return _doc;
  1239. });
  1240. self.records.reset(docs);
  1241. if (queryResult.facets) {
  1242. var facets = _.map(queryResult.facets, function(facetResult, facetId) {
  1243. facetResult.id = facetId;
  1244. return new my.Facet(facetResult);
  1245. });
  1246. self.facets.reset(facets);
  1247. }
  1248. },
  1249. toTemplateJSON: function() {
  1250. var data = this.toJSON();
  1251. data.recordCount = this.recordCount;
  1252. data.fields = this.fields.toJSON();
  1253. return data;
  1254. },
  1255. // ### getFieldsSummary
  1256. //
  1257. // Get a summary for each field in the form of a `Facet`.
  1258. //
  1259. // @return null as this is async function. Provides deferred/promise interface.
  1260. getFieldsSummary: function() {
  1261. var self = this;
  1262. var query = new my.Query();
  1263. query.set({size: 0});
  1264. this.fields.each(function(field) {
  1265. query.addFacet(field.id);
  1266. });
  1267. var dfd = $.Deferred();
  1268. this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
  1269. if (queryResult.facets) {
  1270. _.each(queryResult.facets, function(facetResult, facetId) {
  1271. facetResult.id = facetId;
  1272. var facet = new my.Facet(facetResult);
  1273. // TODO: probably want replace rather than reset (i.e. just replace the facet with this id)
  1274. self.fields.get(facetId).facets.reset(facet);
  1275. });
  1276. }
  1277. dfd.resolve(queryResult);
  1278. });
  1279. return dfd.promise();
  1280. },
  1281. // Deprecated (as of v0.5) - use record.summary()
  1282. recordSummary: function(record) {
  1283. return record.summary();
  1284. },
  1285. // ### _backendFromString(backendString)
  1286. //
  1287. // See backend argument to initialize for details
  1288. _backendFromString: function(backendString) {
  1289. var parts = backendString.split('.');
  1290. // walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
  1291. var current = window;
  1292. for(ii=0;ii<parts.length;ii++) {
  1293. if (!current) {
  1294. break;
  1295. }
  1296. current = current[parts[ii]];
  1297. }
  1298. if (current) {
  1299. return current;
  1300. }
  1301. // alternatively we just had a simple string
  1302. var backend = null;
  1303. if (recline && recline.Backend) {
  1304. _.each(_.keys(recline.Backend), function(name) {
  1305. if (name.toLowerCase() === backendString.toLowerCase()) {
  1306. backend = recline.Backend[name];
  1307. }
  1308. });
  1309. }
  1310. return backend;
  1311. }
  1312. });
  1313. // ## <a id="record">A Record</a>
  1314. //
  1315. // A single record (or row) in the dataset
  1316. my.Record = Backbone.Model.extend({
  1317. constructor: function Record() {
  1318. Backbone.Model.prototype.constructor.apply(this, arguments);
  1319. },
  1320. // ### initialize
  1321. //
  1322. // Create a Record
  1323. //
  1324. // You usually will not do this directly but will have records created by
  1325. // Dataset e.g. in query method
  1326. //
  1327. // Certain methods require presence of a fields attribute (identical to that on Dataset)
  1328. initialize: function() {
  1329. _.bindAll(this, 'getFieldValue');
  1330. },
  1331. // ### getFieldValue
  1332. //
  1333. // For the provided Field get the corresponding rendered computed data value
  1334. // for this record.
  1335. getFieldValue: function(field) {
  1336. val = this.getFieldValueUnrendered(field);
  1337. if (field.renderer) {
  1338. val = field.renderer(val, field, this.toJSON());
  1339. }
  1340. return val;
  1341. },
  1342. // ### getFieldValueUnrendered
  1343. //
  1344. // For the provided Field get the corresponding computed data value
  1345. // for this record.
  1346. getFieldValueUnrendered: function(field) {
  1347. var val = this.get(field.id);
  1348. if (field.deriver) {
  1349. val = field.deriver(val, field, this);
  1350. }
  1351. return val;
  1352. },
  1353. // ### summary
  1354. //
  1355. // Get a simple html summary of this record in form of key/value list
  1356. summary: function(record) {
  1357. var self = this;
  1358. var html = '<div class="recline-record-summary">';
  1359. this.fields.each(function(field) {
  1360. if (field.id != 'id') {
  1361. html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + self.getFieldValue(field) + '</div>';
  1362. }
  1363. });
  1364. html += '</div>';
  1365. return html;
  1366. },
  1367. // Override Backbone save, fetch and destroy so they do nothing
  1368. // Instead, Dataset object that created this Record should take care of
  1369. // handling these changes (discovery will occur via event notifications)
  1370. // WARNING: these will not persist *unless* you call save on Dataset
  1371. fetch: function() {},
  1372. save: function() {},
  1373. destroy: function() { this.trigger('destroy', this); }
  1374. });
  1375. // ## A Backbone collection of Records
  1376. my.RecordList = Backbone.Collection.extend({
  1377. constructor: function RecordList() {
  1378. Backbone.Collection.prototype.constructor.apply(this, arguments);
  1379. },
  1380. model: my.Record
  1381. });
  1382. // ## <a id="field">A Field (aka Column) on a Dataset</a>
  1383. my.Field = Backbone.Model.extend({
  1384. constructor: function Field() {
  1385. Backbone.Model.prototype.constructor.apply(this, arguments);
  1386. },
  1387. // ### defaults - define default values
  1388. defaults: {
  1389. label: null,
  1390. type: 'string',
  1391. format: null,
  1392. is_derived: false
  1393. },
  1394. // ### initialize
  1395. //
  1396. // @param {Object} data: standard Backbone model attributes
  1397. //
  1398. // @param {Object} options: renderer and/or deriver functions.
  1399. initialize: function(data, options) {
  1400. // if a hash not passed in the first argument throw error
  1401. if ('0' in data) {
  1402. throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
  1403. }
  1404. if (this.attributes.label === null) {
  1405. this.set({label: this.id});
  1406. }
  1407. if (options) {
  1408. this.renderer = options.renderer;
  1409. this.deriver = options.deriver;
  1410. }
  1411. if (!this.renderer) {
  1412. this.renderer = this.defaultRenderers[this.get('type')];
  1413. }
  1414. this.facets = new my.FacetList();
  1415. },
  1416. defaultRenderers: {
  1417. object: function(val, field, doc) {
  1418. return JSON.stringify(val);
  1419. },
  1420. geo_point: function(val, field, doc) {
  1421. return JSON.stringify(val);
  1422. },
  1423. 'float': function(val, field, doc) {
  1424. var format = field.get('format');
  1425. if (format === 'percentage') {
  1426. return val + '%';
  1427. }
  1428. return val;
  1429. },
  1430. 'string': function(val, field, doc) {
  1431. var format = field.get('format');
  1432. if (format === 'markdown') {
  1433. if (typeof Showdown !== 'undefined') {
  1434. var showdown = new Showdown.converter();
  1435. out = showdown.makeHtml(val);
  1436. return out;
  1437. } else {
  1438. return val;
  1439. }
  1440. } else if (format == 'plain') {
  1441. return val;
  1442. } else {
  1443. // as this is the default and default type is string may get things
  1444. // here that are not actually strings
  1445. if (val && typeof val === 'string') {
  1446. val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
  1447. }
  1448. return val
  1449. }
  1450. }
  1451. }
  1452. });
  1453. my.FieldList = Backbone.Collection.extend({
  1454. constructor: function FieldList() {
  1455. Backbone.Collection.prototype.constructor.apply(this, arguments);
  1456. },
  1457. model: my.Field
  1458. });
  1459. // ## <a id="query">Query</a>
  1460. my.Query = Backbone.Model.extend({
  1461. constructor: function Query() {
  1462. Backbone.Model.prototype.constructor.apply(this, arguments);
  1463. },
  1464. defaults: function() {
  1465. return {
  1466. size: 100,
  1467. from: 0,
  1468. q: '',
  1469. facets: {},
  1470. filters: []
  1471. };
  1472. },
  1473. _filterTemplates: {
  1474. term: {
  1475. type: 'term',
  1476. // TODO do we need this attribute here?
  1477. field: '',
  1478. term: ''
  1479. },
  1480. range: {
  1481. type: 'range',
  1482. start: '',
  1483. stop: ''
  1484. },
  1485. geo_distance: {
  1486. type: 'geo_distance',
  1487. distance: 10,
  1488. unit: 'km',
  1489. point: {
  1490. lon: 0,
  1491. lat: 0
  1492. }
  1493. }
  1494. },
  1495. // ### addFilter
  1496. //
  1497. // Add a new filter (appended to the list of filters)
  1498. //
  1499. // @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates
  1500. addFilter: function(filter) {
  1501. // crude deep copy
  1502. var ourfilter = JSON.parse(JSON.stringify(filter));
  1503. // not full specified so use template and over-write
  1504. // 3 as for 'type', 'field' and 'fieldType'
  1505. if (_.keys(filter).length <= 3) {
  1506. ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
  1507. }
  1508. var filters = this.get('filters');
  1509. filters.push(ourfilter);
  1510. this.trigger('change:filters:new-blank');
  1511. },
  1512. updateFilter: function(index, value) {
  1513. },
  1514. // ### removeFilter
  1515. //
  1516. // Remove a filter from filters at index filterIndex
  1517. removeFilter: function(filterIndex) {
  1518. var filters = this.get('filters');
  1519. filters.splice(filterIndex, 1);
  1520. this.set({filters: filters});
  1521. this.trigger('change');
  1522. },
  1523. // ### addFacet
  1524. //
  1525. // Add a Facet to this query
  1526. //
  1527. // See <http://www.elasticsearch.org/guide/reference/api/search/facets/>
  1528. addFacet: function(fieldId) {
  1529. var facets = this.get('facets');
  1530. // 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)
  1531. if (_.contains(_.keys(facets), fieldId)) {
  1532. return;
  1533. }
  1534. facets[fieldId] = {
  1535. terms: { field: fieldId }
  1536. };
  1537. this.set({facets: facets}, {silent: true});
  1538. this.trigger('facet:add', this);
  1539. },
  1540. addHistogramFacet: function(fieldId) {
  1541. var facets = this.get('facets');
  1542. facets[fieldId] = {
  1543. date_histogram: {
  1544. field: fieldId,
  1545. interval: 'day'
  1546. }
  1547. };
  1548. this.set({facets: facets}, {silent: true});
  1549. this.trigger('facet:add', this);
  1550. }
  1551. });
  1552. // ## <a id="facet">A Facet (Result)</a>
  1553. my.Facet = Backbone.Model.extend({
  1554. constructor: function Facet() {
  1555. Backbone.Model.prototype.constructor.apply(this, arguments);
  1556. },
  1557. defaults: function() {
  1558. return {
  1559. _type: 'terms',
  1560. total: 0,
  1561. other: 0,
  1562. missing: 0,
  1563. term

Large files files are truncated, but you can click here to view the full file