MTurk HIT Database Mk.II

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

目前為 2015-10-29 提交的版本,檢視 最新版本

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