MTurk HIT Database Mk.II

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

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

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