MTurk HIT Database Mk.II

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

当前为 2015-08-19 提交的版本,查看 最新版本

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