MTurk HIT Database Mk.II

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

当前为 2016-05-01 提交的版本,查看 最新版本

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