MTurk HIT Database Mk.II

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

目前为 2015-09-11 提交的版本。查看 最新版本

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