MTurk HIT Database Mk.II

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

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

  1. // ==UserScript==
  2. // @name MTurk HIT Database Mk.II
  3. // @author feihtality
  4. // @namespace https://greasyfork.org/en/users/12709
  5. // @version 0.7.337
  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:orange;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="TODO PUTLINKTOSCRIPTHERE" class="whatis" target="_blank">(What\'s this?)</a></td></tr>' +
  709. '<tr><td class="container-content" colspan="2">' +
  710. '<div style="text-align:center;" id="hdbDashboardInterface">' +
  711. '<button id="hdbBackup" title="Export your entire database!\nPerfect for moving between computers or as a periodic backup">Create Backup</button>' +
  712. '<button id="hdbRestore" title="Restore database from external backup file" style="margin:5px">Restore</button>' +
  713. '<button id="hdbUpdate" title="Update... the database" style="color:green;">Update Database</button>' +
  714. '<div id="hdbFileSelector" style="display:none"><input id="hdbFileInput" type="file" /></div>' +
  715. '<br>' +
  716. '<button id="hdbPending" title="Summary of all pending HITs\n Can be exported as CSV" style="margin: 0px 5px 5px;">Pending Overview</button>' +
  717. '<button id="hdbRequester" title="Summary of all requesters\n Can be exported as CSV" style="margin: 0px 5px 5px;">Requester Overview</button>' +
  718. '<button id="hdbDaily" title="Summary of each day you\'ve worked\nCan be exported as CSV" style="margin:0px 5px 5px;">Daily Overview</button>' +
  719. '<br>' +
  720. '<label>Find </label>' +
  721. '<select id="hdbStatusSelect"><option value="*">ALL</option><option value="Approval" style="color: orange;">Pending Approval</option>' +
  722. '<option value="Rejected" style="color: red;">Rejected</option><option value="Approved" style="color:green;">Approved - Pending Payment</option>' +
  723. '<option value="(Paid|Approved)" style="color:green;">Paid OR Approved</option></select>' +
  724. '<label> HITs matching: </label><input id="hdbSearchInput" title="Query can be HIT title, HIT ID, or requester name" />' +
  725. '<button id="hdbSearch">Search</button>' +
  726. '<br>' +
  727. '<label>from date </label><input id="hdbMinDate" maxlength="10" size="10" title="Specify a date, or leave blank">' +
  728. '<label> to </label><input id="hdbMaxDate" malength="10" size="10" title="Specify a date, or leave blank">' +
  729. '<label for="hdbCSVInput" title="Export results as CSV file" style="margin-left:50px; vertical-align:middle;">export CSV</label>' +
  730. '<input id="hdbCSVInput" title="Export results as CSV file" type="checkbox" style="vertical-align:middle;">' +
  731. '<br>' +
  732. '<label id="hdbStatusText">placeholder status text</label>' +
  733. '<div id="hdbProgressBar" class="hdbProgressContainer"><div class="hdbProgressOuter"><div class="hdbProgressInner"></div></div></div>' +
  734. '</div></td></tr>';
  735.  
  736. var updateBtn = document.querySelector("#hdbUpdate"),
  737. backupBtn = document.querySelector("#hdbBackup"),
  738. restoreBtn = document.querySelector("#hdbRestore"),
  739. fileInput = document.querySelector("#hdbFileInput"),
  740. exportCSVInput = document.querySelector("#hdbCSVInput"),
  741. searchBtn = document.querySelector("#hdbSearch"),
  742. searchInput = document.querySelector("#hdbSearchInput"),
  743. pendingBtn = document.querySelector("#hdbPending"),
  744. reqBtn = document.querySelector("#hdbRequester"),
  745. dailyBtn = document.querySelector("#hdbDaily"),
  746. fromdate = document.querySelector("#hdbMinDate"),
  747. todate = document.querySelector("#hdbMaxDate"),
  748. statusSelect = document.querySelector("#hdbStatusSelect");
  749.  
  750. var searchResults = document.createElement("DIV");
  751. searchResults.align = "center";
  752. searchResults.id = "hdbSearchResults";
  753. searchResults.style.display = "block";
  754. searchResults.innerHTML = '<table cellSpacing="0" cellpadding="2"></table>';
  755. document.body.insertBefore(searchResults, insertionNode);
  756.  
  757. updateBtn.onclick = function() {
  758. document.querySelector("#hdbProgressBar").style.display = "block";
  759. HITStorage.fetch(MTURK_BASE+"status");
  760. document.querySelector("#hdbStatusText").textContent = "fetching status page....";
  761. };
  762. exportCSVInput.addEventListener("click", function() {
  763. if (exportCSVInput.checked) {
  764. searchBtn.textContent = "Export CSV";
  765. pendingBtn.textContent += " (csv)";
  766. reqBtn.textContent += " (csv)";
  767. dailyBtn.textContent += " (csv)";
  768. }
  769. else {
  770. searchBtn.textContent = "Search";
  771. pendingBtn.textContent = pendingBtn.textContent.replace(" (csv)","");
  772. reqBtn.textContent = reqBtn.textContent.replace(" (csv)","");
  773. dailyBtn.textContent = dailyBtn.textContent.replace(" (csv)", "");
  774. }
  775. });
  776. fromdate.addEventListener("focus", function() {
  777. var offsetX = this.offsetLeft + this.offsetParent.offsetLeft + this.offsetParent.offsetParent.offsetLeft;
  778. var offsetY = this.offsetHeight + this.offsetTop + this.offsetParent.offsetTop + this.offsetParent.offsetParent.offsetTop;
  779. new Calendar(offsetX, offsetY, this).drawCalendar();
  780. });
  781. todate.addEventListener("focus", function() {
  782. var offsetX = this.offsetLeft + this.offsetParent.offsetLeft + this.offsetParent.offsetParent.offsetLeft;
  783. var offsetY = this.offsetHeight + this.offsetTop + this.offsetParent.offsetTop + this.offsetParent.offsetParent.offsetTop;
  784. new Calendar(offsetX, offsetY, this).drawCalendar();
  785. });
  786.  
  787. backupBtn.onclick = HITStorage.backup;
  788. restoreBtn.onclick = function() { fileInput.click(); };
  789. fileInput.onchange = processFile;
  790.  
  791. searchBtn.onclick = function() {
  792. var r = getRange();
  793. var _filter = { status: statusSelect.value, query: searchInput.value.trim().length > 0 ? searchInput.value : "*" };
  794. var _opt = { index: "date", range: r.range, dir: r.dir, filter: _filter };
  795.  
  796. HITStorage.recall("HIT", _opt).then(function(r) {
  797. searchResults.firstChild.innerHTML = r.formatHTML();
  798. autoScroll("#hdbSearchResults", 0.5);
  799. var bonusCells = document.querySelectorAll('td[contenteditable="true"]');
  800. for (var el of bonusCells) {
  801. el.dataset.storedValue = el.textContent;
  802. el.onblur = updateBonus;
  803. el.onkeydown = updateBonus;
  804. }
  805. });
  806. }; // search button click event
  807. pendingBtn.onclick = function() {
  808. var r = getRange();
  809. var _filter = { status: "Approval", query: searchInput.value.trim().length > 0 ? searchInput.value : "*" },
  810. _opt = { index: "date", dir: "prev", range: r.range, filter: _filter };
  811.  
  812. HITStorage.recall("HIT", _opt).then(function(r) {
  813. searchResults.firstChild.innerHTML = r.formatHTML("pending");
  814. autoScroll("#hdbSearchResults", 0.5);
  815. var expands = document.querySelectorAll(".hdbExpandRow");
  816. for (var el of expands) {
  817. el.onclick = showHiddenRows;
  818. }
  819. });
  820. }; //pending overview click event
  821. reqBtn.onclick = function() {
  822. var r = getRange();
  823. var _opt = { index: "date", range: r.range };
  824.  
  825. HITStorage.recall("HIT", _opt).then(function(r) {
  826. searchResults.firstChild.innerHTML = r.formatHTML("requester");
  827. autoScroll("#hdbSearchResults", 0.5);
  828. var expands = document.querySelectorAll(".hdbExpandRow");
  829. for (var el of expands) {
  830. el.onclick = showHiddenRows;
  831. }
  832. });
  833. }; //requester overview click event
  834. dailyBtn.onclick = function() {
  835. HITStorage.recall("STATS", { dir: "prev" }).then(function(r) {
  836. searchResults.firstChild.innerHTML = r.formatHTML("daily");
  837. autoScroll("#hdbSearchResults", 0.5);
  838. });
  839. }; //daily overview click event
  840.  
  841. function getRange() {
  842. var _min = fromdate.value.length === 10 ? fromdate.value : undefined,
  843. _max = todate.value.length === 10 ? todate.value : undefined;
  844. var _range =
  845. (_min === undefined && _max === undefined) ? null :
  846. (_min === undefined) ? window.IDBKeyRange.upperBound(_max) :
  847. (_max === undefined) ? window.IDBKeyRange.lowerBound(_min) :
  848. (_max < _min) ? window.IDBKeyRange.bound(_max,_min) : window.IDBKeyRange.bound(_min,_max);
  849. return { min: _min, max: _max, range: _range, dir: _max < _min ? "prev" : "next" };
  850. }
  851. }//}}} dashboard
  852.  
  853. function showHiddenRows(e) {//{{{
  854. var rid = e.target.parentNode.textContent.substr(4);
  855. var nodes = document.querySelectorAll('tr[data-rid="'+rid+'"]'), el = null;
  856. if (e.target.textContent === "[+]") {
  857. for (el of nodes)
  858. el.style.display="table-row";
  859. e.target.textContent = "[-]";
  860. } else {
  861. for (el of nodes)
  862. el.style.display="none";
  863. e.target.textContent = "[+]";
  864. }
  865. }//}}}
  866.  
  867. function updateBonus(e) {//{{{
  868. if (e instanceof window.KeyboardEvent && e.keyCode === 13) {
  869. e.target.blur();
  870. return false;
  871. } else if (e instanceof window.FocusEvent) {
  872. var _bonus = +e.target.textContent.replace(/\$/,"");
  873. if (_bonus !== +e.target.dataset.storedValue) {
  874. console.log("updating bonus to",_bonus,"from",e.target.dataset.storedValue,"("+e.target.dataset.hitid+")");
  875. e.target.dataset.storedValue = _bonus;
  876. var _pay = +e.target.previousSibling.textContent,
  877. _range = window.IDBKeyRange.only(e.target.dataset.hitid);
  878.  
  879. window.indexedDB.open("HITDB_TESTING").onsuccess = function() {
  880. this.result.transaction("HIT", "readwrite").objectStore("HIT").openCursor(_range).onsuccess = function() {
  881. var c = this.result;
  882. if (c) {
  883. var v = c.value;
  884. v.reward = { pay: _pay, bonus: _bonus };
  885. c.update(v);
  886. }
  887. }; // idbcursor
  888. }; // idbopen
  889. } // bonus is new value
  890. } // keycode
  891. } //}}} updateBonus
  892.  
  893. function processFile(e) {//{{{
  894. 'use strict';
  895.  
  896. var f = e.target.files;
  897. if (f.length && f[0].name.search(/\.bak$/) && f[0].type.search(/text/) >= 0) {
  898. var reader = new FileReader(), testing = true;
  899. reader.readAsText(f[0].slice(0,10));
  900. reader.onload = function(e) {
  901. if (testing && e.target.result.search(/(STATS|NOTES|HIT)/) < 0) {
  902. return error();
  903. } else if (testing) {
  904. testing = false;
  905. reader.readAsText(f[0]);
  906. } else {
  907. var data = JSON.parse(e.target.result);
  908. console.log(data);
  909. HITStorage.write(data, "restore");
  910. }
  911. }; // reader.onload
  912. } else {
  913. error();
  914. }
  915.  
  916. function error() {
  917. var s = document.querySelector("#hdbStatusText"),
  918. e = "Restore::FileReadError : encountered unsupported file";
  919. s.style.color = "red";
  920. s.textContent = e;
  921. throw e;
  922. }
  923. }//}}} processFile
  924.  
  925. // super simple super shitty autoscroll
  926. // TODO make it better
  927. //
  928. function autoScroll(location, time) {//{{{
  929. 'use strict';
  930.  
  931. var target = document.querySelector(location).offsetTop,
  932. pos = window.scrollY,
  933. dpos = Math.floor((target-pos)/300) || 1;
  934. var timer = setInterval(function() {
  935. if (window.scrollY >= target)
  936. clearInterval(timer);
  937. else
  938. window.scrollBy(0, dpos);
  939. }, time*1000/300);
  940. setTimeout(function() { clearInterval(timer); }, 2000);
  941. }//}}}
  942.  
  943. function Calendar(offsetX, offsetY, caller) {//{{{
  944. 'use strict';
  945.  
  946. this.date = new Date();
  947. this.offsetX = offsetX;
  948. this.offsetY = offsetY;
  949. this.caller = caller;
  950. this.drawCalendar = function(year,month,day) {//{{{
  951. year = year || this.date.getFullYear();
  952. month = month || this.date.getMonth()+1;
  953. day = day || this.date.getDate();
  954. var longMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
  955. var date = new Date(year,month-1,day);
  956. var anchors = _getAnchors(date);
  957.  
  958. //make new container if one doesn't already exist
  959. var container = null;
  960. if (document.querySelector("#hdbCalendarPanel")) {
  961. container = document.querySelector("#hdbCalendarPanel");
  962. container.removeChild( container.getElementsByTagName("TABLE")[0] );
  963. }
  964. else {
  965. container = document.createElement("DIV");
  966. container.id = "hdbCalendarPanel";
  967. document.body.appendChild(container);
  968. }
  969. container.style.left = this.offsetX;
  970. container.style.top = this.offsetY;
  971. var cal = document.createElement("TABLE");
  972. cal.cellSpacing = "0";
  973. cal.cellPadding = "0";
  974. cal.border = "0";
  975. container.appendChild(cal);
  976. cal.innerHTML = '<tr>' +
  977. '<th class="hdbCalHeader hdbCalControls" title="Previous month" style="text-align:right;"><span>&lt;</span></th>' +
  978. '<th class="hdbCalHeader hdbCalControls" title="Previous year" style="text-align:center;"><span>&#8810;</span></th>' +
  979. '<th colspan="3" id="hdbCalTableTitle" class="hdbCalHeader">'+date.getFullYear()+'<br>'+longMonths[date.getMonth()]+'</th>' +
  980. '<th class="hdbCalHeader hdbCalControls" title="Next year" style="text-align:center;"><span>&#8811;</span></th>' +
  981. '<th class="hdbCalHeader hdbCalControls" title="Next month" style="text-align:left;"><span>&gt;</span></th>' +
  982. '</tr><tr><th class="hdbDayHeader" style="color:red;">S</th><th class="hdbDayHeader">M</th>' +
  983. '<th class="hdbDayHeader">T</th><th class="hdbDayHeader">W</th><th class="hdbDayHeader">T</th>' +
  984. '<th class="hdbDayHeader">F</th><th class="hdbDayHeader">S</th></tr>';
  985. document.querySelector('th[title="Previous month"]').addEventListener( "click", function() {
  986. this.drawCalendar(date.getFullYear(), date.getMonth(), 1);
  987. }.bind(this) );
  988. document.querySelector('th[title="Previous year"]').addEventListener( "click", function() {
  989. this.drawCalendar(date.getFullYear()-1, date.getMonth()+1, 1);
  990. }.bind(this) );
  991. document.querySelector('th[title="Next month"]').addEventListener( "click", function() {
  992. this.drawCalendar(date.getFullYear(), date.getMonth()+2, 1);
  993. }.bind(this) );
  994. document.querySelector('th[title="Next year"]').addEventListener( "click", function() {
  995. this.drawCalendar(date.getFullYear()+1, date.getMonth()+1, 1);
  996. }.bind(this) );
  997.  
  998. var hasDay = false, thisDay = 1;
  999. for (var i=0;i<6;i++) { // cycle weeks
  1000. var row = document.createElement("TR");
  1001. for (var j=0;j<7;j++) { // cycle days
  1002. if (!hasDay && j === anchors.first && thisDay < anchors.total)
  1003. hasDay = true;
  1004. else if (hasDay && thisDay > anchors.total)
  1005. hasDay = false;
  1006.  
  1007. var cell = document.createElement("TD");
  1008. cell.classList.add("hdbCalCells");
  1009. row.appendChild(cell);
  1010. if (hasDay) {
  1011. cell.classList.add("hdbCalDays");
  1012. cell.textContent = thisDay;
  1013. cell.addEventListener("click", _clickHandler.bind(this));
  1014. cell.dataset.year = date.getFullYear();
  1015. cell.dataset.month = date.getMonth()+1;
  1016. cell.dataset.day = thisDay++;
  1017. }
  1018. } // for j
  1019. cal.appendChild(row);
  1020. } // for i
  1021.  
  1022. function _clickHandler(e) {
  1023. /*jshint validthis:true*/
  1024.  
  1025. var y = e.target.dataset.year;
  1026. var m = Number(e.target.dataset.month).toPadded();
  1027. var d = Number(e.target.dataset.day).toPadded();
  1028. this.caller.value = y+"-"+m+"-"+d;
  1029. this.die();
  1030. }
  1031.  
  1032. function _getAnchors(date) {
  1033. var _anchors = {};
  1034. date.setMonth(date.getMonth()+1);
  1035. date.setDate(0);
  1036. _anchors.total = date.getDate();
  1037. date.setDate(1);
  1038. _anchors.first = date.getDay();
  1039. return _anchors;
  1040. }
  1041. };//}}} drawCalendar
  1042.  
  1043. this.die = function() { document.querySelector("#hdbCalendarPanel").remove(); };
  1044.  
  1045. }//}}} Calendar
  1046. /*
  1047. *
  1048. *
  1049. * * * * * * * * * * * * * TESTING FUNCTIONS -- DELETE BEFORE FINAL RELEASE * * * * * * * * * * *
  1050. *
  1051. *
  1052. */
  1053. function FILEREADERANDBACKUPTESTING() {//{{{
  1054. 'use strict';
  1055. var testdiv = document.createElement("DIV");
  1056. var resultsdiv = document.createElement("DIV");
  1057. document.body.appendChild(testdiv);
  1058. var gobtn = document.createElement("BUTTON");
  1059. var fileinput = document.createElement("INPUT");
  1060. var reader = new FileReader();
  1061. var osinput = document.createElement("INPUT");
  1062. var osgobtn = document.createElement("BUTTON");
  1063. var count = count || 0;
  1064. osgobtn.textContent = "get object store";
  1065. osgobtn.onclick = function() {
  1066. var os = osinput.value || null;
  1067. var backupdata = {};
  1068. if (os) {
  1069. window.indexedDB.open("HITDB_TESTING").onsuccess = function() {
  1070. if (os === "ALL") {
  1071. os = ["BLOCKS", "STATS", "REQUESTER", "HIT", "NOTES"];
  1072. for (var store of os) {
  1073. this.result.transaction(os, "readonly").objectStore(store).openCursor(null).onsuccess = testbackup;
  1074. }
  1075. }
  1076. else {
  1077. var results = [];
  1078. this.result.transaction(os, "readonly").objectStore(os).openCursor(null).onsuccess = function() {
  1079. var cursor = this.result;
  1080. if (cursor) {
  1081. results.push(JSON.stringify(cursor.value));
  1082. cursor.continue();
  1083. } else {
  1084. resultsdiv.innerHTML = results.join("<br>");
  1085. console.log(results);
  1086. }
  1087. }; // cursor
  1088. } // else not "ALL"
  1089. }; //opendb
  1090. } //if os specified
  1091. function testbackup(event) {
  1092. var cursor = event.target.result;
  1093. if (cursor) {
  1094. if (!backupdata[cursor.source.name]) backupdata[cursor.source.name] = [];
  1095. backupdata[cursor.source.name].push(JSON.stringify(cursor.value));
  1096. cursor.continue();
  1097. } else
  1098. if (++count === 5)
  1099. //console.log(count, backupdata);
  1100. finalizebackup();
  1101. }
  1102. function finalizebackup() {
  1103. var backupblob = new Blob([JSON.stringify(backupdata)], {type:""});
  1104. var dl = document.createElement("A");
  1105. dl.href = URL.createObjectURL(backupblob);
  1106. console.log(dl.href);
  1107. dl.download = "hitdb.bak";
  1108. dl.click();
  1109. }
  1110. }; // btn click event
  1111.  
  1112. fileinput.type = "file";
  1113. testdiv.appendChild(fileinput);
  1114. testdiv.appendChild(document.createTextNode("test"));
  1115. testdiv.appendChild(gobtn);
  1116. testdiv.appendChild(osinput);
  1117. testdiv.appendChild(osgobtn);
  1118. testdiv.appendChild(resultsdiv);
  1119. gobtn.textContent = "Go!";
  1120. resultsdiv.style.display = "block";
  1121. resultsdiv.style.height = "500px";
  1122. resultsdiv.style.textAlign = "left";
  1123. testdiv.align = "center";
  1124. gobtn.onclick = function() {
  1125. console.log(fileinput.files);
  1126. if (fileinput.files.length)
  1127. //reader.readAsText(fileinput.files[0].slice(0,100)); // read first 100 chars
  1128. reader.readAsText(fileinput.files[0]);
  1129. };
  1130. reader.onload = function(e) {
  1131. console.log("e:",e);
  1132. console.log(reader);
  1133. var resultsarray = reader.result.split("\n").length;
  1134. var resultsobj = JSON.parse(reader.result);
  1135. console.log(resultsarray);
  1136. console.dir(resultsobj);
  1137. resultsdiv.innerText = reader.result;
  1138. };
  1139.  
  1140. //var reader = new FileReader();
  1141. }//}}}
  1142.  
  1143. function INFLATEDUMMYVALUES() { //{{{
  1144. 'use strict';
  1145.  
  1146. var tdb = this.result;
  1147. tdb.onerror = function(e) { console.log("requesterror",e.target.error.name,e.target.error.message,e); };
  1148. tdb.onversionchange = function(e) { console.log("tdb received versionchange request", e); tdb.close(); };
  1149. //console.log(tdb.transaction("HIT").objectStore("HIT").indexNames.contains("date"));
  1150. console.groupCollapsed("Populating test database");
  1151. var tdbt = {};
  1152. tdbt.trans = tdb.transaction(["HIT", "NOTES"], "readwrite");
  1153. tdbt.hit = tdbt.trans.objectStore("HIT");
  1154. tdbt.notes = tdbt.trans.objectStore("NOTES");
  1155.  
  1156. var filler = { notes:[], hit:[] };
  1157. 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" ];
  1158. for (var n=0;n<filler.hit.length;n++) {
  1159. filler.hit[n] = { date: "2015-08-02", requesterName: "tReq"+(n+1), title: "Best HIT Title #"+(n+1),
  1160. reward: Number((n+1)%(200/n)+(((n+1)%200)/100)).toFixed(2), status: "moo",
  1161. requesterId: String(Math.random(n+1)*312679).replace(".",""), hitId: filler.hit[n] };
  1162. filler.notes[n] = { requesterId: filler.hit[n].requesterId, note: n+1 +
  1163. " Proin vel erat commodo mi interdum rhoncus. Sed lobortis porttitor arcu, et tristique ipsum semper a." +
  1164. " Donec eget aliquet lectus, vel scelerisque ligula." };
  1165. }
  1166.  
  1167. _write(tdbt.hit, filler.hit);
  1168. _write(tdbt.notes, filler.notes);
  1169.  
  1170. function _write(store, obj) {
  1171. if (obj.length) {
  1172. var t = obj.pop();
  1173. store.put(t).onsuccess = function() { _write(store, obj) };
  1174. } else {
  1175. console.log("population complete");
  1176. }
  1177. }
  1178.  
  1179. console.groupEnd();
  1180.  
  1181. dbh = window.indexedDB.open("HITDB_TESTING", DB_VERSION);
  1182. dbh.onerror = function(e) { console.log("[HITDB]",e.target.error.name+":", e.target.error.message, e); };
  1183. console.log(dbh.readyState, dbh);
  1184. dbh.onupgradeneeded = HITStorage.versionChange;
  1185. dbh.onblocked = function(e) { console.log("blocked event triggered:", e); };
  1186.  
  1187. tdb.close();
  1188.  
  1189. }//}}}
  1190.  
  1191. function BLANKSLATE() { //{{{ create empty db equivalent to original schema to test upgrade
  1192. 'use strict';
  1193. var tdb = this.result;
  1194. if (!tdb.objectStoreNames.contains("HIT")) {
  1195. console.log("creating HIT OS");
  1196. var dbo = tdb.createObjectStore("HIT", { keyPath: "hitId" });
  1197. dbo.createIndex("date", "date", { unique: false });
  1198. dbo.createIndex("requesterName", "requesterName", { unique: false});
  1199. dbo.createIndex("title", "title", { unique: false });
  1200. dbo.createIndex("reward", "reward", { unique: false });
  1201. dbo.createIndex("status", "status", { unique: false });
  1202. dbo.createIndex("requesterId", "requesterId", { unique: false });
  1203.  
  1204. }
  1205. if (!tdb.objectStoreNames.contains("STATS")) {
  1206. console.log("creating STATS OS");
  1207. dbo = tdb.createObjectStore("STATS", { keyPath: "date" });
  1208. }
  1209. if (!tdb.objectStoreNames.contains("NOTES")) {
  1210. console.log("creating NOTES OS");
  1211. dbo = tdb.createObjectStore("NOTES", { keyPath: "requesterId" });
  1212. }
  1213. if (!tdb.objectStoreNames.contains("BLOCKS")) {
  1214. console.log("creating BLOCKS OS");
  1215. dbo = tdb.createObjectStore("BLOCKS", { keyPath: "id", autoIncrement: true });
  1216. dbo.createIndex("requesterId", "requesterId", { unique: false });
  1217. }
  1218. } //}}}
  1219.  
  1220.  
  1221.  
  1222. // vim: ts=2:sw=2:et:fdm=marker:noai