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