MTurk HIT Database Mk.II

Keep track of the HITs you've done (and more!). Cross browser compatible.

当前为 2015-10-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MTurk HIT Database Mk.II
  3. // @author feihtality
  4. // @namespace https://greasyfork.org/en/users/12709
  5. // @version 0.9.013
  6. // @description Keep track of the HITs you've done (and more!). Cross browser compatible.
  7. // @include /^https://www\.mturk\.com/mturk/(dash|view|sort|find|prev|search|accept|cont).*/
  8. // @exclude https://www.mturk.com/mturk/findhits?*hit_scraper
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. /**\
  13. **
  14. ** This is a complete rewrite of the MTurk HIT Database script from the ground up, which
  15. ** eliminates obsolete methods, fixes many bugs, and brings this script up-to-date
  16. ** with the current modern browser environment.
  17. **
  18. \**/
  19.  
  20.  
  21. /*
  22. * TODO
  23. * misc refactoring
  24. * tagging (?)
  25. *
  26. */
  27.  
  28.  
  29.  
  30. const DB_VERSION = 2;
  31. const DB_NAME = 'HITDB_TESTING';
  32. const MTURK_BASE = 'https://www.mturk.com/mturk/';
  33.  
  34. /*************************** Native code modifications *******************************/
  35. if (!NodeList.prototype[Symbol.iterator]) NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
  36. Number.prototype.toPadded = function(length) { // format leading zeros
  37. 'use strict';
  38. length = length || 2;
  39. return ("0000000"+this).substr(-length);
  40. };
  41. Math.decRound = function(v, shift) { // decimal rounding
  42. 'use strict';
  43. v = Math.round(+(v+"e"+shift));
  44. return +(v+"e"+-shift);
  45. };
  46. Date.prototype.toLocalISOString = function() { // ISOString by local timezone
  47. 'use strict';
  48. var pad = function(num) { return Number(num).toPadded(); },
  49. offset = pad(Math.floor(this.getTimezoneOffset()/60)) + pad(this.getTimezoneOffset()%60),
  50. timezone = this.getTimezoneOffset() > 0 ? "-" + offset : "+" + offset;
  51. return this.getFullYear() + "-" + pad(this.getMonth()+1) + "-" + pad(this.getDate()) +
  52. "T" + pad(this.getHours()) + ":" + pad(this.getMinutes()) + ":" + pad(this.getSeconds()) + timezone;
  53. };
  54. /***********************************************************************************************/
  55.  
  56. (function() { // simplify strict scoping
  57. 'use strict';
  58.  
  59. var qc = {
  60. extraDays: !!localStorage.getItem("hitdb_extraDays") || false,
  61. fetchData: document.location.pathname === "/mturk/dashboard" ? JSON.parse(localStorage.getItem("hitdb_fetchData") || "{}") : null,
  62. seen: {},
  63. aat: ~document.location.pathname.search(/(dash|accept|cont)/) ? JSON.parse(localStorage.getItem("hitdb_autoAppTemp") || "{}") : null,
  64. save: function(key, name, isObj) {
  65. if (isObj)
  66. localStorage.setItem(name, JSON.stringify(this[key]));
  67. else
  68. localStorage.setItem(name, this[key]);
  69. }
  70. },
  71. metrics = {};
  72.  
  73. var
  74. HITStorage = { //{{{
  75. data: {}, db: null,
  76.  
  77. versionChange: function hsversionChange() { //{{{
  78. var db = this.result;
  79. db.onversionchange = function(e) { console.log("detected version change??",console.dir(e)); db.close(); };
  80. this.onsuccess = function() { db.close(); };
  81. var dbo;
  82.  
  83. console.groupCollapsed("HITStorage.versionChange::onupgradeneeded");
  84.  
  85. if (!db.objectStoreNames.contains("HIT")) {
  86. console.log("creating HIT OS");
  87. dbo = db.createObjectStore("HIT", { keyPath: "hitId" });
  88. dbo.createIndex("date", "date", { unique: false });
  89. dbo.createIndex("requesterName", "requesterName", { unique: false});
  90. dbo.createIndex("title", "title", { unique: false });
  91. dbo.createIndex("reward", "reward", { unique: false });
  92. dbo.createIndex("status", "status", { unique: false });
  93. dbo.createIndex("requesterId", "requesterId", { unique: false });
  94.  
  95. localStorage.setItem("hitdb_extraDays", true);
  96. qc.extraDays = true;
  97. }
  98. if (!db.objectStoreNames.contains("STATS")) {
  99. console.log("creating STATS OS");
  100. dbo = db.createObjectStore("STATS", { keyPath: "date" });
  101. }
  102. if (this.transaction.objectStore("STATS").indexNames.length < 5) { // new in v5: schema additions
  103. this.transaction.objectStore("STATS").createIndex("approved", "approved", { unique: false });
  104. this.transaction.objectStore("STATS").createIndex("earnings", "earnings", { unique: false });
  105. this.transaction.objectStore("STATS").createIndex("pending", "pending", { unique: false });
  106. this.transaction.objectStore("STATS").createIndex("rejected", "rejected", { unique: false });
  107. this.transaction.objectStore("STATS").createIndex("submitted", "submitted", { unique: false });
  108. }
  109.  
  110. (function _updateNotes(dbt) { // new in v5: schema change
  111. if (!db.objectStoreNames.contains("NOTES")) {
  112. console.log("creating NOTES OS");
  113. dbo = db.createObjectStore("NOTES", { keyPath: "id", autoIncrement: true });
  114. dbo.createIndex("hitId", "hitId", { unique: false });
  115. dbo.createIndex("requesterId", "requesterId", { unique: false });
  116. dbo.createIndex("tags", "tags", { unique: false, multiEntry: true });
  117. dbo.createIndex("date", "date", { unique: false });
  118. }
  119. if (db.objectStoreNames.contains("NOTES") && dbt.objectStore("NOTES").indexNames.length < 3) {
  120. _mv(db, dbt, "NOTES", "NOTES", _updateNotes);
  121. }
  122. })(this.transaction);
  123.  
  124. if (db.objectStoreNames.contains("BLOCKS")) {
  125. console.log("migrating BLOCKS to NOTES");
  126. var temp = [];
  127. this.transaction.objectStore("BLOCKS").openCursor().onsuccess = function() {
  128. var cursor = this.result;
  129. if (cursor) {
  130. temp.push( {
  131. requesterId: cursor.value.requesterId,
  132. tags: "Blocked",
  133. note: "This requester was blocked under the old HitDB. Blocking has been deprecated and removed "+
  134. "from HIT Databse. All blocks have been converted to a Note."
  135. } );
  136. cursor.continue();
  137. } else {
  138. console.log("deleting blocks");
  139. db.deleteObjectStore("BLOCKS");
  140. for (var entry of temp)
  141. this.transaction.objectStore("NOTES").add(entry);
  142. }
  143. };
  144. }
  145.  
  146. function _mv(db, transaction, source, dest, fn) { //{{{
  147. var _data = [];
  148. transaction.objectStore(source).openCursor().onsuccess = function() {
  149. var cursor = this.result;
  150. if (cursor) {
  151. _data.push(cursor.value);
  152. cursor.continue();
  153. } else {
  154. db.deleteObjectStore(source);
  155. fn(transaction);
  156. if (_data.length)
  157. for (var i=0;i<_data.length;i++)
  158. transaction.objectStore(dest).add(_data[i]);
  159. //console.dir(_data);
  160. }
  161. };
  162. } //}}}
  163.  
  164. console.groupEnd();
  165. }, // }}} versionChange
  166.  
  167. parseDOM: function(doc) {//{{{
  168. Status.color = "black";
  169.  
  170. var errorCheck = doc.querySelector('td[class="error_title"]');
  171.  
  172. if (doc.title.search(/Status$/) > 0) // status overview
  173. parseStatus();
  174. else if (doc.querySelector('td[colspan="4"]')) // valid status detail, but no data
  175. parseMisc("next");
  176. else if (doc.title.search(/Status Detail/) > 0) // status detail with data
  177. parseDetail();
  178. else if (errorCheck) { // encountered an error page
  179. // hit max request rate
  180. if (~errorCheck.textContent.indexOf("page request rate")) {
  181. var _d = doc.documentURI.match(/\d{8}/)[0],
  182. _p = doc.documentURI.match(/ber=(\d+)/)[1];
  183. metrics.dbupdate.mark("[PRE]"+_d+"p"+_p, "start");
  184. console.log("exceeded max requests; refetching %sp%s", _d, _p);
  185. Status.node.innerHTML = "Exceeded maximum server requests; retrying "+Utils.ISODate(_d)+" page "+_p+"."+
  186. "<br>Please wait...";
  187. setTimeout(HITStorage.fetch, 950, doc.documentURI);
  188. return;
  189. }
  190. // no more staus details left in range
  191. else if (qc.extraDays)
  192. parseMisc("end");
  193. }
  194. else
  195. Utils.errorHandler(new Error("Unhandled document received with URI '" + doc.docuemntURI + "'"));
  196.  
  197.  
  198. function parseStatus() {//{{{
  199. HITStorage.data = { HIT: [], STATS: [] };
  200. qc.seen = {};
  201. ProjectedEarnings.clear();
  202.  
  203. // reload auto-approval data to cover not refreshing the dashboard before running an update
  204. qc.aat = JSON.parse(localStorage.getItem("hitdb_autoAppTemp") || "{}");
  205. qc.aac = JSON.parse(localStorage.getItem("hitdb_autoAppCollection") || "{}");
  206.  
  207. var _pastDataExists = Boolean(Object.keys(qc.fetchData).length);
  208. var raw = {
  209. day: doc.querySelectorAll(".statusDateColumnValue"),
  210. sub: doc.querySelectorAll(".statusSubmittedColumnValue"),
  211. app: doc.querySelectorAll(".statusApprovedColumnValue"),
  212. rej: doc.querySelectorAll(".statusRejectedColumnValue"),
  213. pen: doc.querySelectorAll(".statusPendingColumnValue"),
  214. pay: doc.querySelectorAll(".statusEarningsColumnValue")
  215. };
  216. var timeout = 0;
  217. for (var i=0;i<raw.day.length;i++) {
  218. var d = {};
  219. var _date = raw.day[i].childNodes[1].href.substr(53);
  220. d.date = Utils.ISODate(_date);
  221. d.submitted = +raw.sub[i].textContent;
  222. d.approved = +raw.app[i].textContent;
  223. d.rejected = +raw.rej[i].textContent;
  224. d.pending = +raw.pen[i].textContent;
  225. d.earnings = +raw.pay[i].textContent.substr(1);
  226. HITStorage.data.STATS.push(d);
  227.  
  228. // check whether or not we need to get status detail pages for date, then
  229. // fetch status detail pages per date in range and slightly slow
  230. // down GET requests to avoid making too many in too short an interval
  231. var payload = { encodedDate: _date, pageNumber: 1, sortType: "All" };
  232. if (_pastDataExists) {
  233. // date not in range but is new date (or old date but we need updates)
  234. // lastDate stored in ISO format, fetchData date keys stored in mturk's URI ecnodedDate format
  235. if ( (d.date > qc.fetchData.lastDate) || ~(Object.keys(qc.fetchData).indexOf(_date)) ) {
  236. setTimeout(HITStorage.fetch, timeout, MTURK_BASE+"statusdetail", payload);
  237. timeout += 380;
  238.  
  239. qc.fetchData[_date] = { submitted: d.submitted, pending: d.pending };
  240. }
  241. } else { // get everything
  242. setTimeout(HITStorage.fetch, timeout, MTURK_BASE+"statusdetail", payload);
  243. timeout += 380;
  244.  
  245. qc.fetchData[_date] = { submitted: d.submitted, pending: d.pending };
  246. }
  247. } // for
  248. qc.fetchData.expectedTotal = _calcTotals(qc.fetchData);
  249.  
  250. // try for extra days
  251. if (qc.extraDays === true) {
  252. localStorage.removeItem("hitdb_extraDays");
  253. d = _decDate(HITStorage.data.STATS[HITStorage.data.STATS.length-1].date);
  254. qc.extraDays = d; // repurpose extraDays for QC
  255. payload = { encodedDate: d, pageNumber: 1, sortType: "All" };
  256. setTimeout(HITStorage.fetch, 1000, MTURK_BASE+"statusdetail", payload);
  257. }
  258. console.log(HITStorage.data);
  259. qc.fetchData.lastDate = HITStorage.data.STATS[0].date; // most recent date seen
  260. qc.save("fetchData", "hitdb_fetchData", true);
  261.  
  262. }//}}} parseStatus
  263.  
  264. function parseDetail() {//{{{
  265. var _date = doc.documentURI.replace(/.+(\d{8}).+/, "$1");
  266. var _page = doc.documentURI.replace(/.+ber=(\d+).+/, "$1");
  267.  
  268. metrics.dbupdate.mark("[PRE]"+_date+"p"+_page, "end");
  269. Status.message = "Processing "+Utils.ISODate(_date)+" page "+_page;
  270. var raw = {
  271. req: doc.querySelectorAll(".statusdetailRequesterColumnValue"),
  272. title: doc.querySelectorAll(".statusdetailTitleColumnValue"),
  273. pay: doc.querySelectorAll(".statusdetailAmountColumnValue"),
  274. status: doc.querySelectorAll(".statusdetailStatusColumnValue"),
  275. feedback: doc.querySelectorAll(".statusdetailRequesterFeedbackColumnValue")
  276. };
  277.  
  278. for (var i=0;i<raw.req.length;i++) {
  279. var d = {};
  280. d.date = Utils.ISODate(_date);
  281. d.feedback = raw.feedback[i].textContent.trim();
  282. d.hitId = raw.req[i].childNodes[1].href.replace(/.+HIT\+(.+)/, "$1");
  283. d.requesterId = raw.req[i].childNodes[1].href.replace(/.+rId=(.+?)&.+/, "$1");
  284. d.requesterName = raw.req[i].textContent.trim();
  285. d.reward = +raw.pay[i].textContent.substr(1);
  286. d.status = raw.status[i].textContent.replace(/\s/g, " "); // replace char160 spaces with char32 spaces
  287. d.title = raw.title[i].textContent.trim();
  288.  
  289. // mturk apparently never marks $0.00 HITs as 'Paid' so we fix that
  290. if (!d.reward && ~d.status.search(/approved/i)) d.status = "Paid";
  291. // insert autoApproval times
  292. d.autoAppTime = HITStorage.autoApprovals.getTime(_date,d.hitId);
  293.  
  294. HITStorage.data.HIT.push(d);
  295.  
  296. if (!qc.seen[_date]) qc.seen[_date] = {};
  297. qc.seen[_date] = {
  298. submitted: qc.seen[_date].submitted + 1 || 1,
  299. pending: ~d.status.search(/pending/i) ?
  300. (qc.seen[_date].pending + 1 || 1) : (qc.seen[_date].pending || 0)
  301. };
  302.  
  303. ProjectedEarnings.updateValues(d);
  304. }
  305.  
  306. // additional pages remain; get them
  307. if (doc.querySelector('img[src="/media/right_dbl_arrow.gif"]')) {
  308. var payload = { encodedDate: _date, pageNumber: +_page+1, sortType: "All" };
  309. setTimeout(HITStorage.fetch, 250, MTURK_BASE+"statusdetail", payload);
  310. return;
  311. }
  312.  
  313. if (!qc.extraDays) { // not fetching extra days
  314. //no longer any more useful data here, don't need to keep rechecking this date
  315. if (Utils.ISODate(_date) !== qc.fetchData.lastDate &&
  316. qc.seen[_date].submitted === qc.fetchData[_date].submitted &&
  317. qc.seen[_date].pending === 0) {
  318. console.log("no more pending hits, removing",_date,"from fetchData");
  319. delete qc.fetchData[_date];
  320. qc.save("fetchData", "hitdb_fetchData", true);
  321. HITStorage.autoApprovals.purge(_date);
  322. }
  323. // finished scraping; start writing
  324. console.log("date:", _date, "pages:", _page, "totals:", _calcTotals(qc.seen), "of", qc.fetchData.expectedTotal);
  325. Status.message += " [ "+_calcTotals(qc.seen)+"/"+ qc.fetchData.expectedTotal+" ]";
  326. if (_calcTotals(qc.seen) === qc.fetchData.expectedTotal) {
  327. Status.message = "Writing to database...";
  328. HITStorage.autoApprovals.purge();
  329. HITStorage.write(HITStorage.data, "update");
  330. }
  331. } else if (_date <= qc.extraDays) { // day is older than default range and still fetching extra days
  332. parseMisc("next");
  333. console.log("fetchrequest for", _decDate(Utils.ISODate(_date)));
  334. }
  335. }//}}} parseDetail
  336.  
  337. function parseMisc(type) {//{{{
  338. var _d = doc.documentURI.match(/\d{8}/)[0],
  339. _p = doc.documentURI.match(/ber=(\d+)/)[1];
  340. metrics.dbupdate.mark("[PRE]"+_d+"p"+_p, "end");
  341. var payload = { encodedDate: _decDate(Utils.ISODate(_d)), pageNumber: 1, sortType: "All" };
  342.  
  343. if (type === "next" && +qc.extraDays > 1) {
  344. setTimeout(HITStorage.fetch, 250, MTURK_BASE+"statusdetail", payload);
  345. console.log("going to next page", payload.encodedDate);
  346. } else if (type === "end" && +qc.extraDays > 1) {
  347. Status.message = "Writing to database...";
  348. HITStorage.write(HITStorage.data, "update");
  349. } else
  350. Utils.errorHandler(new TypeError("Failed to execute '"+type+"' in '"+doc.documentURI+"'"));
  351. }//}}}
  352.  
  353. function _decDate(date) {//{{{
  354. var y = date.substr(0,4);
  355. var m = date.substr(5,2);
  356. var d = date.substr(8,2);
  357. date = new Date(y,m-1,d-1);
  358. return Number(date.getMonth()+1).toPadded() + Number(date.getDate()).toPadded() + date.getFullYear();
  359. }//}}}
  360.  
  361. function _calcTotals(obj) {//{{{
  362. var sum = 0;
  363. for (var k in obj){
  364. if (obj.hasOwnProperty(k) && !isNaN(+k))
  365. sum += obj[k].submitted;
  366. }
  367. return sum;
  368. }//}}}
  369. },//}}} parseDOM
  370. autoApprovals: {//{{{
  371. getTime : function(date, hitId) {
  372. if (qc.extraDays || (!Object.keys(qc.aac).length && !Object.keys(qc.aat).length)) return "";
  373. var found = false,
  374. filter = function(id) { return id === hitId; },
  375. autoApp = "";
  376.  
  377. if (qc.aac[date]) {
  378. autoApp = qc.aac[date][Object.keys(qc.aac[date]).filter(filter)[0]] || "";
  379. if (autoApp) found = true;
  380. }
  381. if (!found && Object.keys(qc.aat).length) {
  382. for (var key in qc.aat) { if (qc.aat.hasOwnProperty(key)) { // for all dates in aat
  383. var id = Object.keys(qc.aat[key]).filter(filter)[0];
  384. autoApp = qc.aat[key][id] || "";
  385. if (autoApp) {
  386. found = true;
  387. qc.aac[date] = qc.aac[date] || {};
  388. qc.aac[date][id] = qc.aat[key][id]; // move time from temp var to collection var
  389. delete qc.aat[key][id];
  390. qc.save("aat", "hitdb_autoAppTemp", true);
  391. qc.save("aac", "hitdb_autoAppCollection", true);
  392. break;
  393. }
  394. }} // for key (dates)
  395. } // if !found && aat not empty
  396. return autoApp;
  397. },// getTime
  398. purge : function(date) {
  399. if (date) {
  400. delete qc.aac[date];
  401. qc.save("aac", "hitdb_autoAppCollection", true);
  402. return;
  403. }
  404.  
  405. if (!Object.keys(qc.aat).length) return; // nothing here
  406.  
  407. var pad = function(num) { return Number(num).toPadded(); },
  408. _date = Date.parse(new Date().getFullYear() + "-" + pad(new Date().getMonth()+1) + "-" + pad(new Date().getDate()));
  409.  
  410. for (var key of Object.keys(qc.aat)) {
  411. if (_date - key > 169200000) delete qc.aat[key]; // at least 2 days old, no need to keep it around
  412. }
  413. qc.save("aat", "hitdb_autoAppTemp", true);
  414. } // purge
  415. },//}}} autoApprovals
  416.  
  417. fetch: function(url, payload) { //{{{
  418. //format GET request with query payload
  419. if (payload) {
  420. var args = 0;
  421. url += "?";
  422. for (var k in payload) {
  423. if (payload.hasOwnProperty(k)) {
  424. if (args++) url += "&";
  425. url += k + "=" + payload[k];
  426. }
  427. }
  428. }
  429. // defer XHR to a promise
  430. var fetch = new Promise( function(fulfill, deny) {
  431. var urlreq = new XMLHttpRequest();
  432. urlreq.open("GET", url, true);
  433. urlreq.responseType = "document";
  434. urlreq.send();
  435. urlreq.onload = function() {
  436. if (this.status === 200) {
  437. fulfill(this.response);
  438. } else {
  439. deny(new Error(this.status + " - " + this.statusText));
  440. }
  441. };
  442. urlreq.onerror = function() { deny(new Error(this.status + " - " + this.statusText)); };
  443. urlreq.ontimeout = function() { deny(new Error(this.status + " - " + this.statusText)); };
  444. } );
  445. fetch.then( HITStorage.parseDOM, Utils.errorHandler );
  446.  
  447. }, //}}} fetch
  448. write: function(input, statusUpdate) { //{{{
  449. if (statusUpdate === "update")
  450. qc.timeoutTimer = setTimeout(Utils.errorHandler, 5555, new Error("database access violation"));
  451. if (!HITStorage.db) { Utils.errorHandler(new TypeError('Database is not defined')); return; }
  452.  
  453. var counts = { requests: 0, total: 0 },
  454. os = Object.keys(input),
  455. dbo = [],
  456. dbt = HITStorage.db.transaction(os, "readwrite");
  457. for (var i=0;i<os.length;i++) { // cycle object stores
  458. dbo[i] = dbt.objectStore(os[i]);
  459. for (var k of input[os[i]]) { // cycle entries to put into object stores
  460. if (statusUpdate && ++counts.requests)
  461. dbo[i].put(k).onsuccess = _psfn;
  462. else
  463. dbo[i].put(k);
  464. }
  465. }
  466.  
  467. function _psfn() {
  468. if (++counts.total === counts.requests) {
  469. Status.push(statusUpdate === "update" ? "Update Complete!" :
  470. statusUpdate === "restore" ? "Restoring " + counts.total + " entries... Done!" :
  471. "Done!", "green");
  472. Progress.hide();
  473.  
  474. if (statusUpdate === "update") {
  475. clearTimeout(qc.timeoutTimer);
  476. ProjectedEarnings.data.dbUpdated = new Date().toLocalISOString();
  477. ProjectedEarnings.saveState();
  478. ProjectedEarnings.draw(false);
  479. metrics.dbupdate.stop(); metrics.dbupdate.report();
  480. } else if (statusUpdate === "restore") {
  481. metrics.dbimport.stop(); metrics.dbimport.report();
  482. }
  483. }
  484. }
  485.  
  486. }, //}}} write
  487.  
  488. recall: function(store, options) {//{{{
  489. if (options) {
  490. var index = options.index || null,
  491. range = options.range || null,
  492. dir = options.dir || "next",
  493. limit = options.limit || Infinity;
  494. if (options.filter) {
  495. var fs = options.filter.status !== "*" ? new RegExp(options.filter.status, "i") : false,
  496. fq = options.filter.query !== "*" ? new RegExp(options.filter.query,"i") : false,
  497. fd = options.filter.date || null;
  498. }
  499. if (options.progress)
  500. Progress.show();
  501. } // if options
  502.  
  503. if (!HITStorage.db) { Utils.errorHandler(new TypeError('Database is not defined')); return; }
  504. var sr = new DBResult(), matches = 0, total = 0;
  505. return new Promise( function(resolve) {
  506. var dbo = HITStorage.db.transaction(store, "readonly").objectStore(store), dbq = null;
  507. if (index)
  508. dbq = dbo.index(index).openCursor(range, dir);
  509. else
  510. dbq = dbo.openCursor(range, dir);
  511. dbq.onsuccess = function() {
  512. var c = this.result;
  513. if (c && matches < limit) {
  514. try { Status.message = "Retrieving data... [ " + matches + " / " + (++total) + " ]"; } catch(e) {}
  515. if ( fd && (c.value.date < (fd[0] || "0000") || c.value.date > (fd[1] || "9999")) ) {
  516. c.continue();
  517. return;
  518. }
  519. if ( (!fs && !fq) || // no query filter and no status filter OR
  520. (fs && !fq && ~c.value.status.search(fs)) || // status match and no query filter OR
  521. (!fs && fq && // query match and no status filter OR
  522. (~c.value.title.search(fq) || ~c.value.requesterName.search(fq) || ~c.value.hitId.search(fq))) ||
  523. (fs && fq && ~c.value.status.search(fs) && // status match and query match
  524. (~c.value.title.search(fq) || ~c.value.requesterName.search(fq) || ~c.value.hitId.search(fq))) ) {
  525. sr.include(c.value);
  526. try { Status.message = "Retrieving data... [ " + (++matches) + " / " + total + " ]"; } catch(e) {}
  527. }
  528. c.continue();
  529. } else {
  530. try { Status.message = "Done."; } catch(e) {}
  531. resolve(sr);
  532. }
  533. }; // IDBCursor
  534. }); // promise
  535. },//}}} HITStorage::recall
  536.  
  537. backup: function() {//{{{
  538. var bData = {},
  539. os = ["STATS", "NOTES", "HIT"],
  540. count = 0;
  541.  
  542. Progress.show();
  543. Status.push("Preparing backup...", "black");
  544.  
  545. for (var store of os)
  546. HITStorage.db.transaction(os, "readonly").objectStore(store).openCursor().onsuccess = populateBackup;
  547.  
  548. function populateBackup(e) {
  549. var cursor = e.target.result;
  550. if (cursor) {
  551. if (!bData[cursor.source.name]) bData[cursor.source.name] = [];
  552. bData[cursor.source.name].push(cursor.value);
  553. cursor.continue();
  554. } else
  555. if (++count === 3)
  556. finalizeBackup();
  557. }
  558. function finalizeBackup() {
  559. var backupblob = new Blob([JSON.stringify(bData)], {type:"application/json"});
  560. var date = new Date();
  561. var dl = document.createElement("A");
  562. date = date.getFullYear() + Number(date.getMonth()+1).toPadded() + Number(date.getDate()).toPadded();
  563. dl.href = URL.createObjectURL(backupblob);
  564. console.log(dl.href);
  565. dl.download = "hitdb_"+date+".bak";
  566. document.body.appendChild(dl); // FF doesn't support forced events unless element is part of the document
  567. dl.click(); // so we make it so and click,
  568. dl.remove(); // then immediately remove it
  569. Progress.hide();
  570. Status.push("Done!", "green");
  571. }
  572.  
  573. }//}}} backup
  574.  
  575. }, //}}} HITStorage
  576.  
  577. Utils = { //{{{
  578. ftime : function(t) {//{{{
  579. if (String(t).length && +t === 0) return "0s";
  580. if (!t) return "n/a";
  581. var d = Math.floor(t/86400),
  582. h = Math.floor(t%86400/3600),
  583. m = Math.floor(t%86400%3600/60),
  584. s = t%86400%3600%60;
  585. return ((d>0) ? d+" day"+(d>1 ? "s " : " ") : "") + ((h>0) ? h+"h " : "") + ((m>0) ? m+"m " : "") + ((s>0) ? s+"s" : "");
  586. },//}}}ftime
  587.  
  588. ISODate: function(date) { //{{{ MMDDYYYY <-> YYYY-MM-DD
  589. if (date.length === 10)
  590. return date.substr(5,2)+date.substr(-2)+date.substr(0,4);
  591. else
  592. return date.substr(4)+"-"+date.substr(0,2)+"-"+date.substr(2,2);
  593. },//}}} ISODate
  594.  
  595. getPosition: function(element, includeHeight) {//{{{
  596. var offsets = { x: 0, y: includeHeight ? element.offsetHeight : 0 };
  597. do {
  598. offsets.x += element.offsetLeft;
  599. offsets.y += element.offsetTop;
  600. element = element.offsetParent;
  601. } while (element);
  602. return offsets;
  603. },//}}} getPosition
  604.  
  605. errorHandler: function(err) {//{{{
  606. try { Status.push(err.name + ": " + err.message, "red"); }
  607. catch(e) {}
  608. finally { console.error(err); }
  609. }//}}}
  610.  
  611. }, //}}} Utils
  612.  
  613. ProjectedEarnings = {//{{{
  614. data: JSON.parse(localStorage.getItem("hitdb_projectedEarnings") || "{}"),
  615. updateDate: function() {//{{{
  616. var tableList = document.querySelectorAll(".metrics-table"), el, date;
  617. try {
  618. el = tableList[5].rows[1].cells[0].children[0];
  619. date = el.href.match(/\d{8}/)[0];
  620. } catch(e1) {
  621. try {
  622. el = tableList[3].rows[1].cells[0].children[0];
  623. date = el.href.match(/\d{8}/)[0];
  624. } catch(e2) {
  625. for (var tbl of tableList) {
  626. if (tbl.rows.length < 2 || tbl.rows[1].cells.length < 6) continue;
  627. el = tbl.rows[1].cells[0].children[0];
  628. date = el.href.match(/\d{8}/)[0];
  629. } //for
  630. }//catch
  631. }//catch
  632. var day = el.textContent,
  633. isToday = day === "Today",
  634. _date = new Date(),
  635. pad = function(num) { return Number(num).toPadded(); },
  636. weekEnd = null,
  637. weekStart = null;
  638.  
  639. _date.setDate(_date.getDate() - _date.getDay()); // sunday
  640. weekStart = Date.parse(_date.getFullYear() + "-" + pad(_date.getMonth()+1) + "-" + pad(_date.getDate()));
  641. _date.setDate(_date.getDate() + 7); // next sunday
  642. weekEnd = Date.parse(_date.getFullYear() + "-" + pad(_date.getMonth()+1) + "-" + pad(_date.getDate()));
  643.  
  644. if (!Object.keys(this.data).length) {
  645. this.data = {
  646. today: date, weekStart: weekStart, weekEnd: weekEnd, day: new Date().getDay(), dbUpdated: "n/a",
  647. pending: 0, earnings: {}, target: { day: 0, week: 0 }
  648. };
  649. }
  650.  
  651. if ( (Date.parse(Utils.ISODate(date)) >= this.data.weekEnd) ||
  652. (!isToday && new Date().getDay() < this.data.day) ) { // new week
  653. this.data.earnings = {};
  654. this.data.weekEnd = weekEnd;
  655. this.data.weekStart = weekStart;
  656. }
  657. if ( (this.data.today === null && isToday) || (this.data.today !== null && (date !== this.data.today || !isToday)) ) { // new day
  658. this.data.today = date === this.data.today ? null : date;
  659. this.data.day = new Date().getDay();
  660. }
  661.  
  662. this.saveState();
  663. },//}}} updateDate
  664. draw: function(init) {//{{{
  665. var parentTable = document.querySelector("#total_earnings_amount").offsetParent,
  666. rowPending = init ? parentTable.insertRow(-1) : parentTable.rows[4],
  667. rowProjectedDay = init ? parentTable.insertRow(-1) : parentTable.rows[5],
  668. rowProjectedWeek = init ? parentTable.insertRow(-1) : parentTable.rows[6],
  669. title = "Click to set/change the target value",
  670. weekTotal = this.getWeekTotal(),
  671. dayTotal = this.data.earnings[this.data.today] || 0;
  672.  
  673. if (init) {
  674. rowPending.insertCell(-1);rowPending.insertCell(-1);rowPending.className = "even";
  675. rowProjectedDay.insertCell(-1);rowProjectedDay.insertCell(-1);rowProjectedDay.className = "odd";
  676. rowProjectedWeek.insertCell(-1);rowProjectedWeek.insertCell(-1);rowProjectedWeek.className = "even";
  677. for (var i=0;i<rowPending.cells.length;i++) rowPending.cells[i].style.borderTop = "dotted 1px black";
  678. rowPending.cells[0].className = "metrics-table-first-value";
  679. rowProjectedDay.cells[0].className = "metrics-table-first-value";
  680. rowProjectedWeek.cells[0].className = "metrics-table-first-value";
  681. rowPending.cells[1].title = "This value includes all earnings that are not yet fully cleared as 'Paid'";
  682. }
  683.  
  684. rowPending.cells[0].innerHTML = 'Pending earnings '+
  685. '<span style="font-family:arial;font-size:10px;" title="Timestamp of last database update">[ ' + this.data.dbUpdated + ' ]</span>';
  686. rowPending.cells[1].textContent = "$"+Number(this.data.pending).toFixed(2);
  687. rowProjectedDay.cells[0].innerHTML = 'Projected earnings for the day<br>'+
  688. '<meter id="projectedDayProgress" style="width:220px;" title="'+title+
  689. '" value="'+dayTotal+'" max="'+this.data.target.day+'"></meter>'+
  690. '<span style="color:blue;font-family:arial;font-size:10px;"> ' + Number(dayTotal-this.data.target.day).toFixed(2) + '</span>';
  691. rowProjectedDay.cells[1].textContent = "$"+Number(dayTotal).toFixed(2);
  692. rowProjectedWeek.cells[0].innerHTML = 'Projected earnings for the week<br>' +
  693. '<meter id="projectedWeekProgress" style="width:220px;" title="'+title+
  694. '" value="'+weekTotal+'" max="'+this.data.target.week+'"></meter>' +
  695. '<span style="color:blue;font-family:arial;font-size:10px;"> ' + Number(weekTotal-this.data.target.week).toFixed(2) + '</span>';
  696. rowProjectedWeek.cells[1].textContent = "$"+Number(weekTotal).toFixed(2);
  697.  
  698. document.querySelector("#projectedDayProgress").onclick = updateTargets.bind(this, "day");
  699. document.querySelector("#projectedWeekProgress").onclick = updateTargets.bind(this, "week");
  700.  
  701. function updateTargets(span, e) {
  702. /*jshint validthis:true*/
  703. var goal = prompt("Set your " + (span === "day" ? "daily" : "weekly") + " target:",
  704. this.data.target[span === "day" ? "day" : "week"]);
  705. if (goal && !isNaN(goal)) {
  706. this.data.target[span === "day" ? "day" : "week"] = goal;
  707. e.target.max = goal;
  708. e.target.nextSibling.textContent = " "+Number((span === "day" ? dayTotal : weekTotal) - goal).toFixed(2);
  709. this.saveState();
  710. }
  711. }
  712. },//}}} draw
  713.  
  714. getWeekTotal: function() {
  715. var totals = 0;
  716. for (var k of Object.keys(this.data.earnings))
  717. totals += this.data.earnings[k];
  718.  
  719. return Math.decRound(totals, 2);
  720. },
  721. saveState: function() {
  722. localStorage.setItem("hitdb_projectedEarnings", JSON.stringify(this.data));
  723. },
  724.  
  725. clear: function() {
  726. this.data.pending = 0;
  727. for (var day of Object.keys(this.data.earnings))
  728. if (day in qc.fetchData || day === this.data.today) this.data.earnings[day] = 0;
  729. },
  730.  
  731. updateValues: function(obj) {
  732. var vDate = Date.parse(obj.date), iDate = Utils.ISODate(obj.date);
  733.  
  734. if (~obj.status.search(/pending/i)) // sum pending earnings (include approved until fully cleared as paid)
  735. this.data.pending = Math.decRound(obj.reward+this.data.pending, 2);
  736. if (vDate < this.data.weekEnd && vDate >= this.data.weekStart && !~obj.status.search(/rejected/i)){ // sum weekly earnings by day
  737. this.data.earnings[iDate] = Math.decRound(obj.reward+(this.data.earnings[iDate] || 0), 2 );
  738. }
  739. }
  740. },//}}} ProjectedEarnings
  741.  
  742. DBResult = function(resArr, colObj) {//{{{
  743. this.results = resArr || [];
  744. this.collation = colObj || null;
  745. this.formatHTML = function(type, simple) {//{{{
  746. simple = simple || false;
  747. var count = 0, htmlTxt = [], entry = null, _trClass = null;
  748.  
  749. if (this.results.length < 1) return "<h2>No entries found matching your query.</h2>";
  750.  
  751. if (type === "daily") {
  752. htmlTxt.push('<thead><tr class="hdbHeaderRow"><th></th>'+
  753. '<th>Date</th><th>Submitted</th><th>Approved</th><th>Rejected</th><th>Pending</th><th>Earnings</th></tr></thead><tbody>');
  754. var r = this.collate(this.results,"stats");
  755. for (entry of this.results) {
  756. _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"';
  757. htmlTxt.push('<tr '+_trClass+' style="text-align:right">' +
  758. '<td><span class="hdbExpandRow">[+]</span></td>'+
  759. '<td style="text-align:center;">' + entry.date + '</td><td>' + entry.submitted + '</td>' +
  760. '<td>' + entry.approved + '</td><td>' + entry.rejected + '</td><td>' + entry.pending + '</td>' +
  761. '<td>' + Number(entry.earnings).toFixed(2) + '</td></tr>');
  762. }
  763. htmlTxt.push('</tbody><tfoot><tr class="hdbTotalsRow" style="text-align:right;"><td>Totals:</td>' +
  764. '<td>' + r.totalEntries + ' days</td><td>' + r.totalSub + '</td>' +
  765. '<td>' + r.totalApp + '</td><td>' + r.totalRej + '</td>' +
  766. '<td>' + r.totalPen + '</td><td>$' +
  767. Number(Math.decRound(r.totalPay,2)).toFixed(2) + '</td></tr></tfoot>');
  768. } else if (type === "pending" || type === "requester") {
  769. htmlTxt.push('<thead><tr data-sort="99999" class="hdbHeaderRow"><th>Requester ID</th>' +
  770. '<th width="500">Requester</th><th>' + (type === "pending" ? 'Pending' : 'HITs') + '</th><th>Rewards</th></tr></thead><tbody>');
  771. r = this.collate(this.results,"requesterId");
  772. for (var k in r) {
  773. if (!~k.search(/total/) && r.hasOwnProperty(k)) {
  774. var tr = ['<tr data-sort="'+Math.decRound(r[k].pay,2)+'"><td>' +
  775. '<span class="hdbExpandRow" title="Display all pending HITs from this requester">' +
  776. '[+]</span> ' + r[k][0].requesterId + '</td><td>' + r[k][0].requesterName + '</td>' +
  777. '<td style="text-align:center;">' + r[k].length + '</td><td>' + Number(Math.decRound(r[k].pay,2)).toFixed(2) + '</td></tr>'];
  778.  
  779. for (var hit of r[k]) { // hits in range per requester id
  780. tr.push('<tr data-rid="'+r[k][0].requesterId+'" style="color:#c60000;display:none;"><td style="text-align:right">' +
  781. hit.date + '</td><td width="500" colspan="2">[ <span class="helpSpan" title="Auto-approval time">AA: '+
  782. Utils.ftime(hit.autoAppTime).trim()+'</span> ] '+
  783. hit.title + '</td><td style="text-align:right">' + _parseRewards(hit.reward,"pay") + '</td></tr>');
  784. }
  785. htmlTxt.push(tr.join(''));
  786. }
  787. }
  788. htmlTxt.sort(function(a,b) { return +b.substr(15,5).match(/\d+\.?\d*/) - +a.substr(15,5).match(/\d+\.?\d*/); });
  789. htmlTxt.push('</tbody><tfoot><tr class="hdbTotalsRow"><td style="text-align:right;">Totals:</td>' +
  790. '<td style="text-align:center;">' + (Object.keys(r).length-7) + ' Requesters</td>' +
  791. '<td style="text-align:right;">' + r.totalEntries + '</td>'+
  792. '<td style="text-align:right;">$' + Number(Math.decRound(r.totalPay,2)).toFixed(2) + '</td></tr></tfoot>');
  793. } else { // default
  794. if (!simple)
  795. htmlTxt.push('<thead><tr class="hdbHeaderRow"><th colspan="3"></th>' +
  796. '<th colspan="2" title="Bonuses must be added in manually.\n\nClick inside' +
  797. 'the cell to edit, click out of the cell to save">Reward</th><th colspan="3"></th></tr>'+
  798. '<tr class="hdbHeaderRow">' +
  799. '<th>Date</th><th>Requester</th><th>HIT title</th><th style="font-size:10px;">Pay</th>'+
  800. '<th style="font-size:10px;"><span class="helpSpan" title="Click the cell to edit.\nIts value is automatically saved">'+
  801. 'Bonus</span></th><th>Status</th><th>'+
  802. '<span class="helpSpan" title="Auto-approval times">AA</span></th><th>Feedback</th></tr></thead><tbody>');
  803.  
  804. for (entry of this.results) {
  805. _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"';
  806. var _stColor = ~entry.status.search(/(paid|approved)/i) ? "green" :
  807. entry.status === "Pending Approval" ? "orange" : "red";
  808. var href = MTURK_BASE+'contact?requesterId='+entry.requesterId+'&requesterName='+entry.requesterName+
  809. '&subject=Regarding+Amazon+Mechanical+Turk+HIT+'+entry.hitId;
  810.  
  811. if (!simple)
  812. htmlTxt.push('<tr '+_trClass+' data-id="'+entry.hitId+'">'+
  813. '<td width="74px">' + entry.date + '</td><td style="max-width:145px;">' +
  814. '<a target="_blank" title="Contact this requester" href="'+href+'">' + entry.requesterName + '</a></td>' +
  815. '<td width="375px" title="HIT ID: '+entry.hitId+'">' +
  816. '<span title="Add a note" id="note-'+entry.hitId+'" style="cursor:pointer;">&nbsp;&#128221;&nbsp;</span>' +
  817. entry.title + '</td><td style="text-align:right">' + _parseRewards(entry.reward,"pay") + '</td>' +
  818. '<td style="text-align:right" class="bonusCell" title="Click to add/edit" contenteditable="true" data-hitid="'+entry.hitId+'">' +
  819. (+_parseRewards(entry.reward,"bonus") ? _parseRewards(entry.reward,"bonus") : "") +
  820. '</td><td style="color:'+_stColor+';text-align:center">' + entry.status + '</td>' +
  821. '<td>' + Utils.ftime(entry.autoAppTime) + '</td><td>' + entry.feedback + '</td></tr>');
  822. else
  823. htmlTxt.push('<tr>' + '<td>'+entry.requesterName+'</td>' +
  824. '<td>'+entry.title+'</td><td>'+ _parseRewards(entry.reward,"pay") + '</td><td>'+ entry.status+'</td></tr>');
  825. }
  826.  
  827. if (!simple) {
  828. r = this.collation || this.collate(this.results,"requesterId");
  829. htmlTxt.push('</tbody><tfoot><tr class="hdbTotalsRow"><td></td>' +
  830. '<td style="text-align:right">Totals:</td><td style="text-align:center;">' + r.totalEntries + ' HITs</td>' +
  831. '<td style="text-align:right">$' + Number(Math.decRound(r.totalPay,2)).toFixed(2) + '</td>' +
  832. '<td style="text-align:right">$' + Number(Math.decRound(r.totalBonus,2)).toFixed(2) + '</td>' +
  833. '<td colspan="3"></td></tr></tfoot>');
  834. }
  835. }
  836. return htmlTxt.join('');
  837. };//}}} formatHTML
  838. this.formatCSV = function(type) {//{{{
  839. var csvTxt = [], entry = null, delimiter="\t";
  840. if (type === "daily") {
  841. csvTxt.push( ["Date", "Submitted", "Approved", "Rejected", "Pending", "Earnings\n"].join(delimiter) );
  842. for (entry of this.results) {
  843. csvTxt.push( [entry.date, entry.submitted, entry.approved, entry.rejected,
  844. entry.pending, Number(entry.earnings).toFixed(2)+"\n"].join(delimiter) );
  845. }
  846. csvToFile(csvTxt, "hitdb_dailyOverview.csv");
  847. } else if (type === "pending" || type === "requester") {
  848. csvTxt.push( ["RequesterId","Requester", (type === "pending" ? "Pending" : "HITs"), "Rewards\n"].join(delimiter) );
  849. var r = this.collation || this.collate(this.results,"requesterId");
  850. for (var k in r) {
  851. if (!~k.search(/total/) && r.hasOwnProperty(k))
  852. csvTxt.push( [k, r[k][0].requesterName, r[k].length, Number(Math.decRound(r[k].pay,2)).toFixed(2)+"\n"].join(delimiter) );
  853. }
  854. csvToFile(csvTxt, "hitdb_"+type+"Overview.csv");
  855. } else {
  856. csvTxt.push(["hitId","date","requesterName","requesterId","title","pay","bonus","status","autoAppTime","feedback\n"].join(delimiter));
  857. for (entry of this.results) {
  858. csvTxt.push([entry.hitId, entry.date, entry.requesterName, entry.requesterId, entry.title,
  859. Number(_parseRewards(entry.reward,"pay")).toFixed(2),
  860. (+_parseRewards(entry.reward,"bonus") ? Number(_parseRewards(entry.reward,"bonus")).toFixed(2) : ""),
  861. entry.status, entry.autoAppTime, entry.feedback+"\n"].join(delimiter));
  862. }
  863. csvToFile(csvTxt, "hitdb_queryResults.csv");
  864. }
  865.  
  866. return "<pre>"+csvTxt.join('')+"</pre>";
  867.  
  868. function csvToFile(csv, filename) {
  869. var blob = new Blob(csv, {type: "text/csv", endings: "native"}),
  870. dl = document.createElement("A");
  871. dl.href = URL.createObjectURL(blob);
  872. dl.download = filename;
  873. document.body.appendChild(dl); // FF doesn't support forced events unless element is part of the document
  874. dl.click(); // so we make it so and click,
  875. dl.remove(); // then immediately remove it
  876. return dl;
  877. }
  878. };//}}} formatCSV
  879. this.include = function(value) {
  880. this.results.push(value);
  881. };
  882. this.collate = function(data, index) {//{{{
  883. var r = {
  884. totalPay: 0, totalBonus: 0, totalEntries: data.length,
  885. totalSub: 0, totalApp: 0, totalRej: 0, totalPen: 0
  886. };
  887. for (var e of data) {
  888. if (!r[e[index]]) {
  889. r[e[index]] = [];
  890. Object.defineProperty(r[e[index]], "pay", {value: 0, enumerable: false, configurable: true, writable: true});
  891. }
  892. r[e[index]].push(e);
  893.  
  894. if (index === "stats") {
  895. r.totalSub += e.submitted;
  896. r.totalApp += e.approved;
  897. r.totalRej += e.rejected;
  898. r.totalPen += e.pending;
  899. r.totalPay += e.earnings;
  900. } else {
  901. r[e[index]].pay += (+_parseRewards(e.reward,"pay"));
  902. r.totalPay += (+_parseRewards(e.reward,"pay"));
  903. r.totalBonus += (+_parseRewards(e.reward,"bonus"));
  904. }
  905. }
  906. return r;
  907. };//}}} _collate
  908.  
  909. function _parseRewards(rewards,value) {//{{{
  910. if (!isNaN(rewards)) {
  911. if (value === "pay")
  912. return Number(rewards).toFixed(2);
  913. else
  914. return "0.00";
  915. } else {
  916. if (value === "pay")
  917. return Number(rewards.pay).toFixed(2);
  918. else
  919. return Number(rewards.bonus).toFixed(2);
  920. }
  921. } //}}} _parse
  922. },//}}} databaseresult
  923.  
  924. DashboardUI = {//{{{
  925. draw: function() {//{{{
  926. var controlPanel = document.createElement("TABLE");
  927. var insertionNode = document.querySelector(".footer_separator").previousSibling;
  928. document.body.insertBefore(controlPanel, insertionNode);
  929. controlPanel.width = "760";
  930. controlPanel.align = "center";
  931. controlPanel.id = "hdbControlPanel";
  932. controlPanel.cellSpacing = "0";
  933. controlPanel.cellPadding = "0";
  934. controlPanel.innerHTML = '<tr height="25px"><td width="10" bgcolor="#7FB448" style="padding-left: 10px;"></td>' +
  935. '<td class="white_text_14_bold" style="padding-left:10px; background-color:#7FB448;">' +
  936. 'HIT Database Mk. II&nbsp;<a href="https://greasyfork.org/en/scripts/11733-mturk-hit-database-mk-ii" class="whatis" target="_blank">' +
  937. '(What\'s this?)</a></td></tr>' +
  938. '<tr><td class="container-content" colspan="2">' +
  939. '<div style="text-align:center;" id="hdbDashboardInterface">' +
  940. '<button id="hdbBackup" title="Export your entire database!\nPerfect for moving between computers or as a periodic backup">Create Backup</button>' +
  941. '<button id="hdbRestore" title="Restore database from external backup file" style="margin:5px">Restore</button>' +
  942. '<button id="hdbUpdate" title="Update... the database" style="color:green;">Update Database</button>' +
  943. '<div id="hdbFileSelector" style="display:none"><input id="hdbFileInput" type="file" /></div>' +
  944. '<br>' +
  945. '<button id="hdbPending" title="Summary of all pending HITs\n Can be exported as CSV" style="margin: 0px 5px 5px;">Pending Overview</button>' +
  946. '<button id="hdbRequester" title="Summary of all requesters\n Can be exported as CSV" style="margin: 0px 5px 5px;">Requester Overview</button>' +
  947. '<button id="hdbDaily" title="Summary of each day you\'ve worked\nCan be exported as CSV" style="margin:0px 5px 5px;">Daily Overview</button>' +
  948. '<br>' +
  949. '<label>Find </label>' +
  950. '<select id="hdbStatusSelect"><option value="*">ALL</option>' +
  951. '<option value="Pending Approval" style="color: orange;">Pending Approval</option>' +
  952. '<option value="Rejected" style="color: red;">Rejected</option>' +
  953. '<option value="Approved - Pending Payment" style="color:green;">Approved - Pending Payment</option>' +
  954. '<option value="(Paid|Approved)" style="color:green;">Paid OR Approved</option></select>' +
  955. '<label> HITs matching: </label><input id="hdbSearchInput" title="Query can be HIT title, HIT ID, or requester name" />' +
  956. '<button id="hdbSearch">Search</button>' +
  957. '<br>' +
  958. '<label>from date </label><input id="hdbMinDate" maxlength="10" size="10" title="Specify a date, or leave blank">' +
  959. '<label> to </label><input id="hdbMaxDate" malength="10" size="10" title="Specify a date, or leave blank">' +
  960. '<label for="hdbCSVInput" title="Export results as CSV file" style="margin-left:50px; vertical-align:middle;">export CSV</label>' +
  961. '<input id="hdbCSVInput" title="Export results as CSV file" type="checkbox" style="vertical-align:middle;">' +
  962. '<br>' +
  963. '<label id="hdbStatusText"></label>' +
  964. '<div id="hdbProgressBar" class="hdbProgressContainer"><div class="hdbProgressOuter"><div class="hdbProgressInner"></div></div></div>' +
  965. '</div></td></tr>';
  966.  
  967. var searchResults = document.createElement("DIV");
  968. searchResults.align = "center";
  969. searchResults.id = "hdbSearchResults";
  970. searchResults.style.display = "block";
  971. searchResults.innerHTML =
  972. '<span class="hdbResControl" id="hdbResClear">[ clear results ]</span>' +
  973. '<span class="hdbTablePagination" id="hdbPageTop"></span><br>' +
  974. '<table cellSpacing="0" cellpadding="2" id="hdbResultsTable"></table>' +
  975. '<span class="hdbResControl" id="hdbVpTop">Back to top</span>' +
  976. '<span class="hdbTablePagination" id="hdbPageBot"></span><br>';
  977. document.body.insertBefore(searchResults, insertionNode);
  978. },//}}} dashboardUI::draw
  979.  
  980. initClickables: function() {//{{{
  981. var updateBtn = document.getElementById("hdbUpdate"),
  982. backupBtn = document.getElementById("hdbBackup"),
  983. restoreBtn = document.getElementById("hdbRestore"),
  984. fileInput = document.getElementById("hdbFileInput"),
  985. exportCSVInput = document.getElementById("hdbCSVInput"),
  986. searchBtn = document.getElementById("hdbSearch"),
  987. searchInput = document.getElementById("hdbSearchInput"),
  988. pendingBtn = document.getElementById("hdbPending"),
  989. reqBtn = document.getElementById("hdbRequester"),
  990. dailyBtn = document.getElementById("hdbDaily"),
  991. fromdate = document.getElementById("hdbMinDate"),
  992. todate = document.getElementById("hdbMaxDate"),
  993. statusSelect = document.getElementById("hdbStatusSelect"),
  994. searchResults = document.getElementById("hdbSearchResults"),
  995. resultsTable = document.getElementById("hdbResultsTable");
  996.  
  997. searchResults.firstChild.onclick = function() { //{{{ clear results
  998. resultsTable.innerHTML = null;
  999. for (var d of ["hdbResClear","hdbPageTop","hdbVpTop", "hdbPageBot"]) {
  1000. if (~d.search(/page/i)) d.innerHTML = "";
  1001. document.getElementById(d).style.display = "none";
  1002. }
  1003. };//}}}
  1004. document.getElementById("hdbVpTop").onclick = function() { autoScroll("#hdbControlPanel"); };
  1005.  
  1006. updateBtn.onclick = function() { //{{{
  1007. Progress.show();
  1008. metrics.dbupdate = new Metrics("database_update");
  1009. HITStorage.fetch(MTURK_BASE+"status");
  1010. Status.message = "fetching status page....";
  1011. };//}}}
  1012. exportCSVInput.addEventListener("click", function() {//{{{
  1013. var a = document.getElementById('hdbAnalytics');
  1014. if (a && a.checked) a.click();
  1015. if (exportCSVInput.checked) {
  1016. searchBtn.textContent = "Export CSV";
  1017. pendingBtn.textContent += " (csv)";
  1018. reqBtn.textContent += " (csv)";
  1019. dailyBtn.textContent += " (csv)";
  1020. }
  1021. else {
  1022. searchBtn.textContent = "Search";
  1023. pendingBtn.textContent = pendingBtn.textContent.replace(" (csv)","");
  1024. reqBtn.textContent = reqBtn.textContent.replace(" (csv)","");
  1025. dailyBtn.textContent = dailyBtn.textContent.replace(" (csv)", "");
  1026. }
  1027. });//}}}
  1028. fromdate.addEventListener("focus", function() {//{{{ dates
  1029. var offsets = Utils.getPosition(this, true);
  1030. new Calendar(offsets.x, offsets.y, this).drawCalendar();
  1031. });
  1032. todate.addEventListener("focus", function() {
  1033. var offsets = Utils.getPosition(this, true);
  1034. new Calendar(offsets.x, offsets.y, this).drawCalendar();
  1035. });//}}} dates
  1036.  
  1037. backupBtn.onclick = HITStorage.backup;
  1038. restoreBtn.onclick = function() { fileInput.click(); };
  1039. fileInput.onchange = processFile;
  1040.  
  1041. searchBtn.addEventListener('click', function(e) {//{{{
  1042. if (!/^[se]/i.test(e.target.textContent)) return;
  1043. qc.sr = []; // clear prev pagination
  1044. var r = this.getRange();
  1045. var _filter = { status: statusSelect.value, query: searchInput.value.trim().length > 0 ? searchInput.value : "*" };
  1046. var _opt = { index: r.index, range: r.range, dir: r.dir, filter: _filter, progress: true };
  1047.  
  1048. _dbaccess("search", ["HIT", _opt], function(r) {
  1049. var limiter = 500;
  1050. if (exportCSVInput.checked)
  1051. resultsTable.innerHTML = r.formatCSV();
  1052. else if (r.results.length > limiter) {
  1053. var collation = r.collate(r.results, "requesterId");
  1054. do { qc.sr.push(new DBResult(r.results.splice(0,limiter), collation)) } while (r.results.length);
  1055. resultConstrain(qc.sr, 0);
  1056. } else
  1057. resultConstrain(r);
  1058. });
  1059. }.bind(this)); //}}} search button click event
  1060. //{{{ overview buttons
  1061. pendingBtn.onclick = function() {
  1062. var _filter = { date: [fromdate.value, todate.value], query: searchInput.value.trim().length > 0 ? searchInput.value : "*" },
  1063. _opt = { index: "status", dir: "prev", range: window.IDBKeyRange.only("Pending Approval"), filter: _filter, progress: true };
  1064.  
  1065. _dbaccess("pending", ["HIT", _opt], function(r) {
  1066. resultsTable.innerHTML = exportCSVInput.checked ? r.formatCSV("pending") : r.formatHTML("pending");
  1067. var expands = document.querySelectorAll(".hdbExpandRow");
  1068. for (var el of expands)
  1069. el.onclick = showHiddenRows;
  1070. });
  1071. }.bind(this); //pending overview click event
  1072. reqBtn.onclick = function() {
  1073. var r = this.getRange();
  1074. var _opt = { index: r.index, range: r.range, progress: true };
  1075.  
  1076. _dbaccess("requester", ["HIT", _opt], function(r) {
  1077. resultsTable.innerHTML = exportCSVInput.checked ? r.formatCSV("requester") : r.formatHTML("requester");
  1078. var expands = document.querySelectorAll(".hdbExpandRow");
  1079. for (var el of expands)
  1080. el.onclick = showHiddenRows;
  1081. });
  1082. }.bind(this); //requester overview click event
  1083. dailyBtn.onclick = function() {
  1084. var r = this.getRange("*");
  1085. _dbaccess("daily", ["STATS", { range: r.range, dir: "prev", progress: true }], function(r) {
  1086. resultsTable.innerHTML = exportCSVInput.checked ? r.formatCSV("daily") : r.formatHTML("daily");
  1087. var expands = document.querySelectorAll(".hdbExpandRow");
  1088. for (var el of expands)
  1089. el.onclick = showHitsByDate;
  1090. });
  1091. }.bind(this); //daily overview click event
  1092. //}}}
  1093.  
  1094. function _dbaccess(method, rargs, tfn) {//{{{
  1095. Status.push("Preparing database...", "black");
  1096. metrics.dbrecall = new Metrics("database_recall::"+method);
  1097. metrics.dbrecall.mark("data retrieval", "start");
  1098.  
  1099. HITStorage.recall(rargs[0],rargs[1]).then(function(r) {
  1100. metrics.dbrecall.mark("data retrieval", "end");
  1101. Status.message = "Building HTML...";
  1102. try {
  1103. for (var d of ["hdbResClear","hdbPageTop","hdbVpTop", "hdbPageBot"]) {
  1104. if (~d.search(/page/i) && method !== "search") continue;
  1105. document.getElementById(d).style.display = "initial";
  1106. }
  1107. metrics.dbrecall.mark("HTML construction", "start");
  1108. tfn(r);
  1109. metrics.dbrecall.mark("HTML construction", "end");
  1110. } catch(e) {
  1111. Utils.errorHandler(e);
  1112. } finally {
  1113. autoScroll("#hdbSearchResults");
  1114. Status.push("Done!", "green");
  1115. Progress.hide();
  1116. metrics.dbrecall.stop(); metrics.dbrecall.report();
  1117. }
  1118. });
  1119. }//}}} _dbaccess
  1120. },//}}} dashboardUI::initClickables
  1121.  
  1122. getRange: function(status) {//{{{
  1123. var fromdate = document.getElementById("hdbMinDate"),
  1124. todate = document.getElementById("hdbMaxDate"),
  1125. statusSelect = document.getElementById("hdbStatusSelect");
  1126. var _min = fromdate.value.length > 3 ? fromdate.value : undefined,
  1127. _max = todate.value.length > 3 ? todate.value : undefined;
  1128. status = status || statusSelect.value;
  1129. var _range =
  1130. (_min === undefined && _max === undefined) ?
  1131. (status.length > 1 && !~status.search(/\(/) ? window.IDBKeyRange.only(status) : null) :
  1132. (_min === undefined) ? window.IDBKeyRange.upperBound(_max) :
  1133. (_max === undefined) ? window.IDBKeyRange.lowerBound(_min) :
  1134. (_max < _min) ? window.IDBKeyRange.bound(_max,_min) : window.IDBKeyRange.bound(_min,_max),
  1135. _index = _min === undefined && _max === undefined && status.length > 1 && !~status.search(/\(/) ? "status" : "date";
  1136. return { min: _min, max: _max, range: _range, dir: _max < _min ? "prev" : "next", index: _index };
  1137. }//}}} dashboardUI::getRange
  1138. };//}}} dashboard
  1139.  
  1140. /*
  1141. *
  1142. *
  1143. *
  1144. *
  1145. *///{{{
  1146. // the Set() constructor is never actually used other than to test for Chrome v38+
  1147. // might want to bump requirement up to v45 for those sweet, sweet arrow functions...
  1148. if (!("indexedDB" in window && "Set" in window)) alert("HITDB::Your browser is too outdated or otherwise incompatible with this script!");
  1149. else {
  1150. /*
  1151. var tdbh = window.indexedDB.open(DB_NAME);
  1152. tdbh.onerror = function(e) { console.log("[TESTDB]",e.target.error.name+":", e.target.error.message, e); };
  1153. tdbh.onsuccess = INFLATEDUMMYVALUES;
  1154. //tdbh.onupgradeneeded = BLANKSLATE;
  1155. var dbh = null;
  1156. */
  1157. if (document.location.pathname === "/mturk/dashboard") {
  1158. var dbh = window.indexedDB.open(DB_NAME, DB_VERSION);
  1159. dbh.onerror = function(e) { Utils.errorHandler(e.target.error); };
  1160. dbh.onupgradeneeded = HITStorage.versionChange;
  1161. dbh.onsuccess = function() { HITStorage.db = this.result; };
  1162.  
  1163. DashboardUI.draw();
  1164. DashboardUI.initClickables();
  1165.  
  1166. ProjectedEarnings.updateDate();
  1167. ProjectedEarnings.draw(true);
  1168.  
  1169. var Status = {
  1170. node: document.getElementById("hdbStatusText"),
  1171. get message() { return this.node.textContent; },
  1172. set message(str) { this.node.textContent = str; },
  1173. get color() { return this.node.style.color; },
  1174. set color(c) { this.node.style.color = c; },
  1175. push: function(m,c) { c = c || "black"; this.message = m; this.color = c; }
  1176. };
  1177. var Progress = {
  1178. node: document.getElementById("hdbProgressBar"),
  1179. hide: function() { this.node.style.display = "none"; },
  1180. show: function() { this.node.style.display = "block"; }
  1181. };
  1182. // export some variables for external extensions
  1183. window.Status = Status; window.Progress = Progress, window.Metrics = Metrics;
  1184. } else { // page is not dashboard
  1185. window.indexedDB.open(DB_NAME).onsuccess = function() { HITStorage.db = this.result; beenThereDoneThat(); }
  1186. }
  1187. }
  1188. /*}}}
  1189. *
  1190. *
  1191. *
  1192. *
  1193. */
  1194.  
  1195. // {{{ css injection
  1196. var css = "<style type='text/css'>" +
  1197. ".hitdbRTButtons {border:1px solid; font-size: 10px; height: 18px; padding-left: 5px; padding-right: 5px; background: pink;}" +
  1198. ".hitdbRTButtons-green {background: lightgreen;}" +
  1199. ".hitdbRTButtons-large {width:80px;}" +
  1200. ".hdbProgressContainer {margin:auto; width:500px; height:6px; position:relative; display:none; border-radius:10px; overflow:hidden; background:#d3d8db;}" +
  1201. ".hdbProgressInner {width:100%; position:absolute; left:0;top:0;bottom:0; animation: kfpin 1.4s infinite; background:" +
  1202. "linear-gradient(262deg, rgba(208,69,247,0), rgba(208,69,247,1), rgba(69,197,247,1), rgba(69,197,247,0)); background-size: 300% 500%;}" +
  1203. ".hdbProgressOuter {width:30%; position:absolute; left:0;top:0;bottom:0; animation: kfpout 2s cubic-bezier(0,0.55,0.2,1) infinite;}" +
  1204. "@keyframes kfpout { 0% {left:-100%;} 70%{left:100%;} 100%{left:100%;} }" +
  1205. "@keyframes kfpin { 0%{background-position: 0% 50%} 50%{background-position: 100% 15%} 100%{background-position:0% 30%} }" +
  1206. ".hdbCalControls {cursor:pointer;} .hdbCalControls:hover {color:c27fcf;}" +
  1207. ".hdbCalCells {background:#f0f6f9; height:19px}" +
  1208. ".hdbCalDays {cursor:pointer; text-align:center;} .hdbCalDays:hover {background:#7fb4cf; color:white;}" +
  1209. ".hdbDayHeader {width:26px; text-align:center; font-weight:bold; font-size:12px; background:#f0f6f9;}" +
  1210. ".hdbCalHeader {background:#7fb4cf; color:white; font-weight:bold; text-align:center; font-size:11px; padding:3px 0px;}" +
  1211. "#hdbCalendarPanel {position:absolute; z-index:10; box-shadow:-2px 3px 5px 0px rgba(0,0,0,0.68);}" +
  1212. ".hdbExpandRow {cursor:pointer; color:blue;}" +
  1213. ".hdbTotalsRow {background:#CCC; color:#369; font-weight:bold;}" +
  1214. ".hdbHeaderRow {background:#7FB448; font-size:12px; color:white}" +
  1215. ".helpSpan {border-bottom:1px dotted; cursor:help;}" +
  1216. ".hdbResControl {border-bottom:1px solid; color:#c60; cursor:pointer; display:none;}" +
  1217. ".hdbTablePagination {margin-left:15em; color:#c60; display:none;}" +
  1218. ".spin {animation: kfspin 0.7s infinite linear; font-weight:bold;}" +
  1219. "@keyframes kfspin { 0% { transform: rotate(0deg) } 100% { transform: rotate(359deg) } }" +
  1220. ".spin:before{content:'*'}" +
  1221. "</style>";
  1222. document.head.innerHTML += css;
  1223. // }}}
  1224.  
  1225. function resultConstrain(data, index) {//{{{
  1226. data = data || qc.sr;
  1227.  
  1228. var table = document.getElementById("hdbResultsTable"),
  1229. rslice = data.length ? data[index].results : data.results,
  1230. pager = [document.getElementById("hdbPageTop"), document.getElementById("hdbPageBot")],
  1231. sopt = [];
  1232. pager[0].innerHTML = ''; pager[1].innerHTML = '';
  1233.  
  1234. if (data instanceof DBResult)
  1235. table.innerHTML = data.formatHTML();
  1236. else {
  1237. table.innerHTML = data[index].formatHTML();
  1238. pager[0].innerHTML = '<span style="cursor:pointer;">' + (index > 0 ? '&#9664; Prev' : '') + '</span> ' +
  1239. '<span style="cursor:pointer;">' + (+index+1 === data.length ? '' : 'Next &#9654;') + '</span> &nbsp; || &nbsp; '+
  1240. '<label>Select page: </label><select></select>';
  1241. for (var i=0;i<data.length;i++) {
  1242. if (i === +index)
  1243. sopt.push('<option value="' + i + '" selected="selected">' + (i+1) + '</option>');
  1244. else
  1245. sopt.push('<option value="' + i + '">' + (i+1) + '</option>');
  1246. }
  1247. pager[0].lastChild.innerHTML = sopt.join('');
  1248. pager[2] = pager[0].cloneNode(true);
  1249. pager[2].id = "hdbPageBot";
  1250. for (i of [0,2]) {
  1251. pager[i].children[0].onclick = resultConstrain.bind(null,null,+index-1);
  1252. pager[i].children[1].onclick = resultConstrain.bind(null,null,+index+1);
  1253. pager[i].children[3].onchange = _f;
  1254. }
  1255. pager[0].parentNode.replaceChild(pager[2], pager[1]);
  1256. }
  1257.  
  1258. for (var _r of rslice) { // retrieve and append notes
  1259. HITStorage.recall("NOTES", { index: "hitId", range: window.IDBKeyRange.only(_r.hitId) }).then(noteHandler.bind(null,"attach"));
  1260. }
  1261.  
  1262. var _nodes = [document.querySelectorAll(".bonusCell"), document.querySelectorAll('span[id^="note-"]')];
  1263. for (i=0;i<_nodes[0].length;i++) {
  1264. var bonus = _nodes[0][i],
  1265. note = _nodes[1][i];
  1266. bonus.dataset.initial = bonus.textContent;
  1267. bonus.onkeydown = updateBonus;
  1268. bonus.onblur = updateBonus;
  1269. note.onclick = noteHandler.bind(null,"new");
  1270. }
  1271.  
  1272. // to avoid defining a function within a loop
  1273. function _f(e) { resultConstrain(null,e.target.value); }
  1274. }//}}} resultConstrain
  1275.  
  1276. function beenThereDoneThat() {//{{{
  1277. if (~document.location.pathname.search(/(accept|continue)/)) {
  1278. if (!document.querySelector('input[name="hitAutoAppDelayInSeconds"]')) return;
  1279.  
  1280. // capture autoapproval times
  1281. var _aa = document.querySelector('input[name="hitAutoAppDelayInSeconds"]').value,
  1282. _hid = document.querySelectorAll('input[name="hitId"]')[1].value,
  1283. pad = function(num) { return Number(num).toPadded(); },
  1284. _d = Date.parse(new Date().getFullYear() + "-" + pad(new Date().getMonth()+1) + "-" + pad(new Date().getDate()));
  1285.  
  1286. if (!qc.aat[_d]) qc.aat[_d] = {};
  1287. qc.aat[_d][_hid] = _aa;
  1288. qc.save("aat", "hitdb_autoAppTemp", true);
  1289. return;
  1290. }
  1291. var qualNode = document.querySelector('td[colspan="11"]');
  1292. if (qualNode) { // we're on the preview page!
  1293. var requesterid = document.querySelector('input[name="requesterId"]').value,
  1294. requestername = document.querySelector('input[name="prevRequester"]').value,
  1295. autoApproval = document.querySelector('input[name="hitAutoAppDelayInSeconds"]').value,
  1296. hitTitle = document.querySelector('div[style*="ellipsis"]').textContent.trim(),
  1297. insertionNode = qualNode.parentNode.parentNode;
  1298. var row = document.createElement("TR"), cellL = document.createElement("TD"), cellR = document.createElement("TD");
  1299. var resultsTableR = document.createElement("TABLE"),
  1300. resultsTableT = document.createElement("TABLE");
  1301. resultsTableR.dataset.rid = requesterid;
  1302. resultsTableT.dataset.title = hitTitle;
  1303. insertionNode.parentNode.parentNode.appendChild(resultsTableR);
  1304. insertionNode.parentNode.parentNode.appendChild(resultsTableT);
  1305.  
  1306. cellR.innerHTML = '<span class="capsule_field_title">Auto-Approval:</span>&nbsp;&nbsp;'+Utils.ftime(autoApproval);
  1307. var rbutton = document.createElement("BUTTON");
  1308. rbutton.classList.add("hitdbRTButtons","hitdbRTButtons-large");
  1309. rbutton.textContent = "Requester";
  1310. rbutton.onclick = function(e) { e.preventDefault(); showResults.call(resultsTableR, "req", hitTitle); };
  1311. var tbutton = rbutton.cloneNode(false);
  1312. rbutton.title = "Show HITs completed from this requester";
  1313. tbutton.textContent = "HIT Title";
  1314. tbutton.onclick = function(e) { e.preventDefault(); showResults.call(resultsTableT, "title", requestername) };
  1315. HITStorage.recall("HIT", {index: "requesterId", range: window.IDBKeyRange.only(requesterid), limit: 1})
  1316. .then(processResults.bind(rbutton,resultsTableR));
  1317. HITStorage.recall("HIT", {index: "title", range: window.IDBKeyRange.only(hitTitle), limit: 1})
  1318. .then(processResults.bind(tbutton,resultsTableT));
  1319. row.appendChild(cellL);
  1320. row.appendChild(cellR);
  1321. cellL.appendChild(rbutton);
  1322. cellL.appendChild(tbutton);
  1323. cellL.colSpan = "3";
  1324. cellR.colSpan = "8";
  1325. insertionNode.appendChild(row);
  1326. } else { // browsing HITs n sutff
  1327. var titleNodes = document.querySelectorAll('a[class="capsulelink"]');
  1328. if (titleNodes.length < 1) return; // nothing left to do here!
  1329. var requesterNodes = document.querySelectorAll('a[href*="hitgroups&requester"]');
  1330. var insertionNodes = [];
  1331.  
  1332. for (var i=0;i<titleNodes.length;i++) {
  1333. var _title = titleNodes[i].textContent.trim();
  1334. var _tbutton = document.createElement("BUTTON");
  1335. var _id = requesterNodes[i].href.replace(/.+Id=(.+)/, "$1");
  1336. var _name = requesterNodes[i].textContent;
  1337. var _rbutton = document.createElement("BUTTON");
  1338. var _div = document.createElement("DIV"), _tr = document.createElement("TR");
  1339. resultsTableR = document.createElement("TABLE");
  1340. resultsTableR.dataset.rid = _id;
  1341. resultsTableT = document.createElement("TABLE");
  1342. resultsTableT.dataset.title = _title;
  1343. insertionNodes.push(requesterNodes[i].parentNode.parentNode.parentNode);
  1344. insertionNodes[i].offsetParent.offsetParent.offsetParent.offsetParent.appendChild(resultsTableR);
  1345. insertionNodes[i].offsetParent.offsetParent.offsetParent.offsetParent.appendChild(resultsTableT);
  1346.  
  1347. HITStorage.recall("HIT", {index: "title", range: window.IDBKeyRange.only(_title), limit: 1} )
  1348. .then(processResults.bind(_tbutton,resultsTableT));
  1349. HITStorage.recall("HIT", {index: "requesterId", range: window.IDBKeyRange.only(_id), limit: 1} )
  1350. .then(processResults.bind(_rbutton,resultsTableR));
  1351.  
  1352. _tr.appendChild(_div);
  1353. _div.id = "hitdbRTInjection-"+i;
  1354. _div.appendChild(_rbutton);
  1355. _rbutton.textContent = 'R';
  1356. _rbutton.classList.add("hitdbRTButtons");
  1357. _rbutton.onclick = showResults.bind(resultsTableR, "req", _title);
  1358. _rbutton.title = "Show HITs completed from this requester";
  1359. _div.appendChild(_tbutton);
  1360. _tbutton.textContent = 'T';
  1361. _tbutton.classList.add("hitdbRTButtons");
  1362. _tbutton.onclick = showResults.bind(resultsTableT, "title", _name);
  1363. insertionNodes[i].appendChild(_tr);
  1364. }
  1365. } // else
  1366.  
  1367. function showResults(type, match) {//{{{
  1368. /*jshint validthis: true*/
  1369. if (!this.dataset.hasResults) return;
  1370. if (this.children.length) // table is populated
  1371. this.innerHTML = '';
  1372. else { // need to populate table
  1373. var head = this.createTHead(),
  1374. body = this.createTBody(),
  1375. capt = this.createCaption(),
  1376. style= "font-size:10px;font-weight:bold;text-align:center",
  1377. validKeys = function(obj) { return Object.keys(obj).filter(function(v) { return !~v.search(/total[A-Z]/); }); };
  1378.  
  1379. capt.innerHTML = '<span style="'+style+'">Loading...<label class="spin"></label></span>';
  1380.  
  1381. if (type === "req") {
  1382. HITStorage.recall("HIT", {index:"requesterId", range:window.IDBKeyRange.only(this.dataset.rid)})
  1383. .then( function(r) {
  1384. var cbydate = r.collate(r.results, "date"),
  1385. kbydate = validKeys(cbydate),
  1386. cbydatextitle, kbytitle, bodyHTML = [];
  1387. kbydate.forEach(function(date) {
  1388. cbydatextitle = r.collate(cbydate[date], "title");
  1389. kbytitle = validKeys(cbydatextitle);
  1390. kbytitle.forEach(function(title) {
  1391. bodyHTML.push('<tr style="text-align:center;"><td>'+date+'</td>' +
  1392. '<td style="text-align:left">'+title.trim()+'</td><td>'+cbydatextitle[title].length+'</td>' +
  1393. '<td>'+Number(Math.decRound(cbydatextitle[title].pay,2)).toFixed(2)+'</td></tr>');
  1394. });
  1395. });
  1396. var help = "Total number of HITs submitted for a given date with the same title\n" +
  1397. "(aggregates results with the same title to simplify the table and reduce unnecessary spam for batch workers)";
  1398. head.innerHTML = '<tr style="'+style+'"><th>Date</th><th>Title</th>' +
  1399. '<th><span class="helpSpan" title="'+help+'">#HITs</span></th><th>Total Rewards</th></tr>';
  1400. body.innerHTML = bodyHTML.sort(function(a,b) {
  1401. return a.match(/\d{4}-\d{2}-\d{2}/)[0] < b.match(/\d{4}-\d{2}-\d{2}/)[0] ? 1 : -1;
  1402. }).join('');
  1403. capt.innerHTML = '<label style="'+style+'">HITs Matching This Requester</label>';
  1404.  
  1405. var mrows = Array.prototype.filter.call(body.rows, function(v) {return v.cells[1].textContent === match});
  1406. for (var row of mrows)
  1407. row.style.background = "lightgreen";
  1408. });
  1409. }
  1410. else if (type === "title") {
  1411. HITStorage.recall("HIT", {index:"title", range:window.IDBKeyRange.only(this.dataset.title)})
  1412. .then( function(r) {
  1413. var cbyreq = r.collate(r.results, "requesterName"),
  1414. kbyreq = validKeys(cbyreq),
  1415. bodyHTML = [];
  1416. for (var key of kbyreq)
  1417. bodyHTML.push('<tr style="text-align:center;"><td>'+key+'</td><td>'+cbyreq[key].length+'</td>' +
  1418. '<td>'+Number(Math.decRound(cbyreq[key].pay,2)).toFixed(2)+'</td></tr>');
  1419. var help = "Total number of HITs matching this title submitted for a given requester\n" +
  1420. "(aggregates results with the same requester name to simplify the table and reduce unnecessary spam for batch workers)";
  1421. head.innerHTML = '<tr style="'+style+'"><th>Requester Name</th>' +
  1422. '<th><span class="helpSpan" title="'+help+'">#HITs</span></th><th>Total Rewards</th></tr>';
  1423. body.innerHTML = bodyHTML.join('');
  1424. capt.innerHTML = '<label style="'+style+'">Reqesters With HITs Matching This Title</label>';
  1425.  
  1426. var mrows = Array.prototype.filter.call(body.rows, function(v) {return v.cells[0].textContent === match});
  1427. for (var row of mrows)
  1428. row.style.background = "lightgreen";
  1429. });
  1430. } //if type === 'title'
  1431. }//populate table
  1432. }//}}} showResults
  1433.  
  1434. function processResults(table, r) {
  1435. /*jshint validthis: true*/
  1436. if (r.results.length) {
  1437. table.dataset.hasResults = "true";
  1438. this.classList.add("hitdbRTButtons-green");
  1439. }
  1440. }
  1441. }//}}} btdt
  1442.  
  1443. function showHiddenRows(e) {//{{{
  1444. var rid = e.target.parentNode.textContent.substr(4);
  1445. var nodes = document.querySelectorAll('tr[data-rid="'+rid+'"]'), el = null;
  1446. if (e.target.textContent === "[+]") {
  1447. for (el of nodes)
  1448. el.style.display="table-row";
  1449. e.target.textContent = "[-]";
  1450. } else {
  1451. for (el of nodes)
  1452. el.style.display="none";
  1453. e.target.textContent = "[+]";
  1454. }
  1455. }//}}}
  1456.  
  1457. function showHitsByDate(e) {//{{{
  1458. var date = e.target.parentNode.nextSibling.textContent,
  1459. row = e.target.parentNode.parentNode,
  1460. table= row.parentNode;
  1461.  
  1462. if (e.target.textContent === "[+]") {
  1463. e.target.textContent = "[-]";
  1464. var nrow = table.insertBefore(document.createElement("TR"), row.nextSibling);
  1465. nrow.className = row.className;
  1466. nrow.innerHTML = '<td><b>Loading...<label class="spin"></label></b></td>';
  1467. HITStorage.recall("HIT", {index: "date", range: window.IDBKeyRange.only(date)}).then( function(r) {
  1468. r.results = r.results.sort(function(a,b) {if (a.requesterName.toLowerCase() > b.requesterName.toLowerCase()) return 1; else return -1;});
  1469. nrow.innerHTML = '<td colspan="7"><table style="width:440;color:#c60;">' + r.formatHTML(null,true) + '</table></td>';
  1470. });
  1471. } else {
  1472. e.target.textContent = "[+]";
  1473. table.removeChild(row.nextSibling);
  1474. }
  1475. }//}}} showHitsByDate
  1476.  
  1477. function updateBonus(e) {//{{{
  1478. if (e instanceof window.KeyboardEvent && e.keyCode === 13) {
  1479. e.target.blur();
  1480. return false;
  1481. } else if (e instanceof window.FocusEvent) {
  1482. var _bonus = +e.target.textContent.replace(/\$/,"");
  1483. if (_bonus !== +e.target.dataset.initial) {
  1484. console.log("updating bonus to",_bonus,"from",e.target.dataset.initial,"("+e.target.dataset.hitid+")");
  1485. e.target.dataset.initial = _bonus;
  1486. var _pay = +e.target.previousSibling.textContent,
  1487. _range = window.IDBKeyRange.only(e.target.dataset.hitid);
  1488.  
  1489. HITStorage.db.transaction("HIT", "readwrite").objectStore("HIT").openCursor(_range).onsuccess = function() {
  1490. var c = this.result;
  1491. if (c) {
  1492. var v = c.value;
  1493. v.reward = { pay: _pay, bonus: _bonus };
  1494. c.update(v);
  1495. }
  1496. }; // idbcursor
  1497. } // bonus is new value
  1498. } // keycode
  1499. } //}}} updateBonus
  1500.  
  1501. function noteHandler(type, e) {//{{{
  1502. //
  1503. // TODO restructure event handling/logic tree
  1504. // combine save and delete; it's ugly :(
  1505. // actually this whole thing is messy and in need of refactoring
  1506. //
  1507. if (e instanceof window.KeyboardEvent) {
  1508. if (e.keyCode === 13) {
  1509. e.target.blur();
  1510. return false;
  1511. }
  1512. return;
  1513. }
  1514.  
  1515. if (e instanceof window.FocusEvent) {
  1516. if (e.target.textContent.trim() !== e.target.dataset.initial) {
  1517. if (!e.target.textContent.trim()) { e.target.previousSibling.previousSibling.firstChild.click(); return; }
  1518. var note = e.target.textContent.trim(),
  1519. _range = window.IDBKeyRange.only(e.target.dataset.id),
  1520. inote = e.target.dataset.initial,
  1521. hitId = e.target.dataset.id,
  1522. date = e.target.previousSibling.textContent;
  1523.  
  1524. e.target.dataset.initial = note;
  1525. HITStorage.db.transaction("NOTES", "readwrite").objectStore("NOTES").index("hitId").openCursor(_range).onsuccess = function() {
  1526. if (this.result) {
  1527. var r = this.result.value;
  1528. if (r.note === inote) { // note already exists in database, so we update its value
  1529. r.note = note;
  1530. this.result.update(r);
  1531. return;
  1532. }
  1533. this.result.continue();
  1534. } else {
  1535. if (this.source instanceof window.IDBObjectStore)
  1536. this.source.put({ note:note, date:date, hitId:hitId });
  1537. else
  1538. this.source.objectStore.put({ note:note, date:date, hitId:hitId });
  1539. }
  1540. };
  1541. }
  1542. return; // end of save event; no need to proceed
  1543. }
  1544.  
  1545. if (type === "delete") {
  1546. var tr = e.target.parentNode.parentNode,
  1547. noteCell = tr.lastChild;
  1548. _range = window.IDBKeyRange.only(noteCell.dataset.id);
  1549. if (!noteCell.dataset.initial) tr.remove();
  1550. else {
  1551. HITStorage.db.transaction("NOTES", "readwrite").objectStore("NOTES").index("hitId").openCursor(_range).onsuccess = function() {
  1552. if (this.result) {
  1553. if (this.result.value.note === noteCell.dataset.initial) {
  1554. this.result.delete();
  1555. tr.remove();
  1556. return;
  1557. }
  1558. this.result.continue();
  1559. }
  1560. };
  1561. }
  1562. return; // end of deletion event; no need to proceed
  1563. } else {
  1564. if (type === "attach" && !e.results.length) return;
  1565.  
  1566. var trow = e instanceof window.MouseEvent ? e.target.parentNode.parentNode : null,
  1567. tbody = trow ? trow.parentNode : null,
  1568. row = document.createElement("TR"),
  1569. c1 = row.insertCell(0),
  1570. c2 = row.insertCell(1),
  1571. c3 = row.insertCell(2);
  1572. date = new Date();
  1573. hitId = e instanceof window.MouseEvent ? e.target.id.substr(5) : null;
  1574.  
  1575. c1.innerHTML = '<span class="removeNote" title="Delete this note" style="cursor:pointer;color:crimson;">[x]</span>';
  1576. c1.firstChild.onclick = noteHandler.bind(null,"delete");
  1577. c1.style.textAlign = "right";
  1578. c2.title = "Date on which the note was added";
  1579. c3.style.color = "crimson";
  1580. c3.colSpan = "6";
  1581. c3.contentEditable = "true";
  1582. c3.onblur = noteHandler.bind(null,"blur");
  1583. c3.onkeydown = noteHandler.bind(null, "kb");
  1584. if (type === "new") {
  1585. row.classList.add(trow.classList);
  1586. tbody.insertBefore(row, trow.nextSibling);
  1587. c2.textContent = date.getFullYear()+"-"+Number(date.getMonth()+1).toPadded()+"-"+Number(date.getDate()).toPadded();
  1588. c3.dataset.initial = "";
  1589. c3.dataset.id = hitId;
  1590. c3.focus();
  1591. return;
  1592. }
  1593.  
  1594. for (var entry of e.results) {
  1595. trow = document.querySelector('tr[data-id="'+entry.hitId+'"]');
  1596. tbody = trow.parentNode;
  1597. row = row.cloneNode(true);
  1598. c1 = row.firstChild;
  1599. c2 = c1.nextSibling;
  1600. c3 = row.lastChild;
  1601. row.classList.add(trow.classList);
  1602. tbody.insertBefore(row, trow.nextSibling);
  1603.  
  1604. c1.firstChild.onclick = noteHandler.bind(null,"delete");
  1605. c2.textContent = entry.date;
  1606. c3.textContent = entry.note;
  1607. c3.dataset.initial = entry.note;
  1608. c3.dataset.id = entry.hitId;
  1609. c3.onblur = noteHandler.bind(null,"blur");
  1610. c3.onkeydown = noteHandler.bind(null, "kb");
  1611. }
  1612. } // new/attach
  1613. }//}}} noteHandler
  1614.  
  1615. function processFile(e) {//{{{
  1616. var f = e.target.files;
  1617. if (f.length && ~f[0].name.search(/\.(bak|csv|json)$/)/* && ~f[0].type.search(/(text|json)/)*/) {
  1618. var reader = new FileReader(), testing = true, isCsv = false;
  1619. metrics.dbimport = new Metrics("file_import");
  1620.  
  1621. reader.readAsText(f[0].slice(0,10));
  1622. reader.onload = function(e) {
  1623. var r = e.target.result;
  1624. if (testing && !~r.search(/(STATS|NOTES|HIT)/)) { // failed json check, test if csv
  1625. console.log("failed json integrity:", r, "\nchecking csv schema...");
  1626. if (!~r.search(/hitId/)) { // failed csv check, return error
  1627. console.log("failed csv integrity:", r, "\naborting");
  1628. return Utils.errorHandler(new TypeError("Invalid data structure"));
  1629. } else { // passed initial csv check, parse full file
  1630. console.log("deferring to csv parser");
  1631. isCsv = true;
  1632. testing = false;
  1633. Progress.show();
  1634. reader.readAsText(f[0]);
  1635. }
  1636. } else if (testing) {
  1637. testing = false;
  1638. Progress.show();
  1639. reader.readAsText(f[0]);
  1640. } else {
  1641. if (isCsv) // if csv defer to csv parser
  1642. return parseCsv(r);
  1643. // otherwise write the json
  1644. var data = JSON.parse(r);
  1645. console.log(data);
  1646. HITStorage.write(data, "restore");
  1647. }
  1648. }; // reader.onload
  1649. } else if (f.length) {
  1650. Utils.errorHandler(new TypeError("Unsupported file format"));
  1651. }
  1652. }//}}} processFile
  1653.  
  1654. function parseCsv(r) {//{{{
  1655. var validKeys = ["autoAppTime","date","feedback","hitId","requesterId","requesterName","reward","pay","bonus","status","title"],
  1656. delimiter = r.substr(5,1),
  1657. lines = r.split(/\r?\n/),
  1658. columns = lines.splice(0,1)[0].split(delimiter),
  1659. data = { HIT:[] },
  1660. badCsv = false;
  1661.  
  1662. if (!columns[columns.length-1].length) { badCsv = true; void(columns.splice(columns.length-1)); }
  1663. if (!lines.length) return Utils.errorHandler(new Error("CSV file must contain at least one record"));
  1664. // make sure column keys are valid
  1665. for (var key of columns) {
  1666. if (!~validKeys.indexOf(key)) {
  1667. Progress.hide();
  1668. return Utils.errorHandler(new TypeError("Invalid key '"+key+"' found in column header"));
  1669. }
  1670. }
  1671. // iterate through each line and convert data into a usable object
  1672. var ln = 1;
  1673. for (var line of lines) {
  1674. var record = {};
  1675. line = badCsv ? line.replace(new RegExp(delimiter+"$"),"").split(delimiter) : line.split(delimiter);
  1676. if (line.length <= 1) continue;
  1677. if (++ln && line.length !== columns.length) {
  1678. // attempt to resolve delimiter conflicts within field values
  1679. line = line.join(delimiter).replace(new RegExp('(?:(")'+delimiter+'|'+delimiter+'("))',"g"),'$1\t$2').split(/\t/);
  1680. // resolution failed
  1681. if (line.length !== columns.length)
  1682. return Utils.errorHandler(new RangeError("Number of fields does not match number of columns in line '"+ln+"'"));
  1683. }
  1684.  
  1685. for (var i=0;i<line.length;i++) {
  1686. if (columns[i] === "pay" || columns[i] === "bonus") {
  1687. record.reward = record.reward || {};
  1688. record.reward[columns[i]] = +line[i];
  1689. continue;
  1690. }
  1691. if (columns[i] === "reward")
  1692. record[columns[i]] = +line[i];
  1693. else
  1694. record[columns[i]] = badCsv ? line[i].replace(/(^"|"$)/g,"") : line[i];
  1695. }
  1696. data.HIT.push(record);
  1697. }
  1698. console.log(data);
  1699. HITStorage.write(data, "restore");
  1700. }//}}} parseCsv
  1701.  
  1702. function autoScroll(location, dt) {//{{{
  1703. var target = document.querySelector(location).offsetTop,
  1704. pos = window.scrollY,
  1705. dpos = Math.ceil((target - pos)/3);
  1706. dt = dt ? dt-1 : 25; // time step/max recursions
  1707.  
  1708. if (target === pos || dpos === 0 || dt === 0) return;
  1709.  
  1710. window.scrollBy(0, dpos);
  1711. setTimeout(function() { autoScroll(location, dt); }, dt);
  1712. }//}}}
  1713.  
  1714. function Calendar(offsetX, offsetY, caller) {//{{{
  1715. this.date = new Date();
  1716. this.offsetX = offsetX;
  1717. this.offsetY = offsetY;
  1718. this.caller = caller;
  1719. this.drawCalendar = function(year,month,day) {//{{{
  1720. year = year || this.date.getFullYear();
  1721. month = month || this.date.getMonth()+1;
  1722. day = day || this.date.getDate();
  1723. var longMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
  1724. var date = new Date(year,month-1,day);
  1725. var anchors = _getAnchors(date);
  1726.  
  1727. //make new container if one doesn't already exist
  1728. var container = null;
  1729. if (document.querySelector("#hdbCalendarPanel")) {
  1730. container = document.querySelector("#hdbCalendarPanel");
  1731. container.removeChild( container.getElementsByTagName("TABLE")[0] );
  1732. }
  1733. else {
  1734. container = document.createElement("DIV");
  1735. container.id = "hdbCalendarPanel";
  1736. document.body.appendChild(container);
  1737. }
  1738. container.style.left = this.offsetX;
  1739. container.style.top = this.offsetY;
  1740. var cal = document.createElement("TABLE");
  1741. cal.cellSpacing = "0";
  1742. cal.cellPadding = "0";
  1743. cal.border = "0";
  1744. container.appendChild(cal);
  1745. cal.innerHTML = '<tr>' +
  1746. '<th class="hdbCalHeader hdbCalControls" title="Previous month" style="text-align:right;"><span>&lt;</span></th>' +
  1747. '<th class="hdbCalHeader hdbCalControls" title="Previous year" style="text-align:center;"><span>&#8810;</span></th>' +
  1748. '<th colspan="3" id="hdbCalTableTitle" class="hdbCalHeader">'+date.getFullYear()+'<br>'+longMonths[date.getMonth()]+'</th>' +
  1749. '<th class="hdbCalHeader hdbCalControls" title="Next year" style="text-align:center;"><span>&#8811;</span></th>' +
  1750. '<th class="hdbCalHeader hdbCalControls" title="Next month" style="text-align:left;"><span>&gt;</span></th>' +
  1751. '</tr><tr><th class="hdbDayHeader" style="color:red;">S</th><th class="hdbDayHeader">M</th>' +
  1752. '<th class="hdbDayHeader">T</th><th class="hdbDayHeader">W</th><th class="hdbDayHeader">T</th>' +
  1753. '<th class="hdbDayHeader">F</th><th class="hdbDayHeader">S</th></tr>';
  1754. document.querySelector('th[title="Previous month"]').addEventListener( "click", function() {
  1755. this.drawCalendar(date.getFullYear(), date.getMonth(), 1);
  1756. }.bind(this) );
  1757. document.querySelector('th[title="Previous year"]').addEventListener( "click", function() {
  1758. this.drawCalendar(date.getFullYear()-1, date.getMonth()+1, 1);
  1759. }.bind(this) );
  1760. document.querySelector('th[title="Next month"]').addEventListener( "click", function() {
  1761. this.drawCalendar(date.getFullYear(), date.getMonth()+2, 1);
  1762. }.bind(this) );
  1763. document.querySelector('th[title="Next year"]').addEventListener( "click", function() {
  1764. this.drawCalendar(date.getFullYear()+1, date.getMonth()+1, 1);
  1765. }.bind(this) );
  1766.  
  1767. var hasDay = false, thisDay = 1;
  1768. for (var i=0;i<6;i++) { // cycle weeks
  1769. var row = document.createElement("TR");
  1770. for (var j=0;j<7;j++) { // cycle days
  1771. if (!hasDay && j === anchors.first && thisDay < anchors.total)
  1772. hasDay = true;
  1773. else if (hasDay && thisDay > anchors.total)
  1774. hasDay = false;
  1775.  
  1776. var cell = document.createElement("TD");
  1777. cell.classList.add("hdbCalCells");
  1778. row.appendChild(cell);
  1779. if (hasDay) {
  1780. cell.classList.add("hdbCalDays");
  1781. cell.textContent = thisDay;
  1782. cell.addEventListener("click", _clickHandler.bind(this));
  1783. cell.dataset.year = date.getFullYear();
  1784. cell.dataset.month = date.getMonth()+1;
  1785. cell.dataset.day = thisDay++;
  1786. }
  1787. } // for j
  1788. cal.appendChild(row);
  1789. } // for i
  1790. var controls = cal.insertRow(-1);
  1791. controls.insertCell(0);
  1792. controls.cells[0].colSpan = "7";
  1793. controls.cells[0].classList.add("hdbCalCells");
  1794. controls.cells[0].innerHTML = ' &nbsp; &nbsp; <a href="javascript:void(0)" style="font-weight:bold;text-decoration:none;">Clear</a>' +
  1795. ' &nbsp; <a href="javascript:void(0)" style="font-weight:bold;text-decoration:none;">Close</a>';
  1796. controls.cells[0].children[0].onclick = function() { this.caller.value = ""; }.bind(this);
  1797. controls.cells[0].children[1].onclick = this.die;
  1798.  
  1799. function _clickHandler(e) {
  1800. /*jshint validthis:true*/
  1801.  
  1802. var y = e.target.dataset.year;
  1803. var m = Number(e.target.dataset.month).toPadded();
  1804. var d = Number(e.target.dataset.day).toPadded();
  1805. this.caller.value = y+"-"+m+"-"+d;
  1806. this.die();
  1807. }
  1808.  
  1809. function _getAnchors(date) {
  1810. var _anchors = {};
  1811. date.setMonth(date.getMonth()+1);
  1812. date.setDate(0);
  1813. _anchors.total = date.getDate();
  1814. date.setDate(1);
  1815. _anchors.first = date.getDay();
  1816. return _anchors;
  1817. }
  1818. };//}}} drawCalendar
  1819.  
  1820. this.die = function() { document.getElementById('hdbCalendarPanel').remove(); };
  1821.  
  1822. }//}}} Calendar
  1823.  
  1824. // instance metrics apart from window scoped PerformanceTiming API
  1825. function Metrics(name) {//{{{
  1826. this.name = name || "undefined";
  1827. this.marks = {};
  1828. this.start = window.performance.now();
  1829. this.end = null;
  1830. this.stop = function(){
  1831. if (!this.end)
  1832. this.end = window.performance.now();
  1833. else
  1834. Utils.errorHandler(new Error("Metrics::AccessViolation - end point cannot be overwritten"));
  1835. };
  1836. this.mark = function(name,position) {
  1837. if (position === "end" && (!this.marks[name] || this.marks[name].end)) return;
  1838.  
  1839. if (!this.marks[name])
  1840. this.marks[name] = {};
  1841.  
  1842. this.marks[name][position] = window.performance.now();
  1843. };
  1844. this.report = function() {
  1845. console.group("Metrics for",this.name.toUpperCase());
  1846. console.log("Process completed in",+Number((this.end-this.start)/1000).toFixed(3),"seconds");
  1847. for (var k in this.marks) {
  1848. if (this.marks.hasOwnProperty(k)) {
  1849. console.log(k,"occurred after",+Number((this.marks[k].start-this.start)/1000).toFixed(3),"seconds,",
  1850. "resolving in", +Number((this.marks[k].end-this.marks[k].start)/1000).toFixed(3), "seconds");
  1851. }
  1852. }
  1853. console.groupEnd();
  1854. };
  1855. }//}}}
  1856.  
  1857. })(); //scoping
  1858.  
  1859. /*
  1860. *
  1861. *
  1862. * * * * * * * * * * * * * TESTING FUNCTIONS -- DELETE BEFORE FINAL RELEASE * * * * * * * * * * *
  1863. *
  1864. *
  1865. */
  1866.  
  1867.  
  1868. function INFLATEDUMMYVALUES() { //{{{
  1869. 'use strict';
  1870.  
  1871. var tdb = this.result;
  1872. tdb.onerror = function(e) { console.log("requesterror",e.target.error.name,e.target.error.message,e); };
  1873. tdb.onversionchange = function(e) { console.log("tdb received versionchange request", e); tdb.close(); };
  1874. //console.log(tdb.transaction("HIT").objectStore("HIT").indexNames.contains("date"));
  1875. console.groupCollapsed("Populating test database");
  1876. var tdbt = {};
  1877. //tdbt.trans = tdb.transaction(["HIT", "NOTES", "BLOCKS"], "readwrite");
  1878. tdbt.trans = tdb.transaction("HIT", "readwrite");
  1879. tdbt.hit = tdbt.trans.objectStore("HIT");
  1880. //tdbt.notes = tdbt.trans.objectStore("NOTES");
  1881. //tdbt.blocks= tdbt.trans.objectStore("BLOCKS");
  1882.  
  1883. var filler = { notes:[], hit:[], blocks:[] };
  1884. for (var n=0;n<100000;n++) {
  1885. filler.hit.push({ date: "2015-08-00", requesterName: "testRequester", title: "Greatest Title Ever #"+(n+1),
  1886. reward: Number((n+1)%(200/n)+(((n+1)%200)/100)).toFixed(2), status: "moo",
  1887. requesterId: ("RRRRRRR"+n).substr(-7), hitId: ("HHHHHHH"+n).substr(-7) });
  1888. /*if (n%1000 === 0) {
  1889. filler.notes.push({ requesterId: ("RRRRRRR"+n).substr(-7), note: n+1 +
  1890. " Proin vel erat commodo mi interdum rhoncus. Sed lobortis porttitor arcu, et tristique ipsum semper a." +
  1891. " Donec eget aliquet lectus, vel scelerisque ligula." });
  1892. filler.blocks.push({requesterId: ("RRRRRRR"+n).substr(-7)});
  1893. }*/
  1894. }
  1895.  
  1896. _write(tdbt.hit, filler.hit);
  1897. _write(tdbt.notes, filler.notes);
  1898. //_write(tdbt.blocks, filler.blocks);
  1899.  
  1900. function _write(store, obj) {
  1901. if (obj.length) {
  1902. var t = obj.pop();
  1903. store.put(t).onsuccess = function() { _write(store, obj) };
  1904. } else {
  1905. console.log("population complete");
  1906. }
  1907. }
  1908.  
  1909. console.groupEnd();
  1910. /*
  1911. var dbh = window.indexedDB.open(DB_NAME, DB_VERSION);
  1912. dbh.onerror = function(e) { console.log("[HITDB]",e.target.error.name+":", e.target.error.message, e); };
  1913. console.log(dbh.readyState, dbh);
  1914. dbh.onupgradeneeded = HITStorage.versionChange;
  1915. dbh.onblocked = function(e) { console.log("blocked event triggered:", e); };
  1916. */
  1917. tdb.close();
  1918.  
  1919. }//}}}
  1920. /*
  1921. function BLANKSLATE() { //{{{ create empty db equivalent to original schema to test upgrade
  1922. 'use strict';
  1923. var tdb = this.result;
  1924. if (!tdb.objectStoreNames.contains("HIT")) {
  1925. console.log("creating HIT OS");
  1926. var dbo = tdb.createObjectStore("HIT", { keyPath: "hitId" });
  1927. dbo.createIndex("date", "date", { unique: false });
  1928. dbo.createIndex("requesterName", "requesterName", { unique: false});
  1929. dbo.createIndex("title", "title", { unique: false });
  1930. dbo.createIndex("reward", "reward", { unique: false });
  1931. dbo.createIndex("status", "status", { unique: false });
  1932. dbo.createIndex("requesterId", "requesterId", { unique: false });
  1933.  
  1934. }
  1935. if (!tdb.objectStoreNames.contains("STATS")) {
  1936. console.log("creating STATS OS");
  1937. dbo = tdb.createObjectStore("STATS", { keyPath: "date" });
  1938. }
  1939. if (!tdb.objectStoreNames.contains("NOTES")) {
  1940. console.log("creating NOTES OS");
  1941. dbo = tdb.createObjectStore("NOTES", { keyPath: "requesterId" });
  1942. }
  1943. if (!tdb.objectStoreNames.contains("BLOCKS")) {
  1944. console.log("creating BLOCKS OS");
  1945. dbo = tdb.createObjectStore("BLOCKS", { keyPath: "id", autoIncrement: true });
  1946. dbo.createIndex("requesterId", "requesterId", { unique: false });
  1947. }
  1948. } //}}}
  1949. */
  1950.  
  1951.  
  1952. // vim: ts=2:sw=2:et:fdm=marker:noai