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