MTurk HIT Database Mk.II

Keep track of the HITs you've done (and more!)

目前为 2015-08-31 提交的版本。查看 最新版本

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