MTurk HIT Database Mk.II

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

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

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