Hermes HIT exporter

Adds an Export button to MTurk HIT capsules to share HITs on forums.

当前为 2016-07-11 提交的版本,查看 最新版本

// ==UserScript==
// @name         Hermes HIT exporter
// @namespace    mobiusevalon.tibbius.com
// @version      2.0
// @author       Mobius Evalon <[email protected]>
// @description  Adds an Export button to MTurk HIT capsules to share HITs on forums.
// @license      Creative Commons Attribution-ShareAlike 4.0; http://creativecommons.org/licenses/by-sa/4.0/
// @require      https://code.jquery.com/jquery-1.12.4.min.js
// @include      /^https{0,1}:\/\/\w{0,}\.?mturk\.com\/mturk\/(?:searchbar|viewsearchbar|sortsearchbar|findhits|viewhits|sorthits)/
// @grant        none
// ==/UserScript==

$(document).ready(function() {
	script_version = "2.0 beta";
	function hit_info($element) {
		function highlight_qual(q) {
			var h = (q.indexOf("Masters") > -1);
			return ((h ? "[color=red]" : "")+q.collapse_whitespace()+(h ? "[/color]" : ""));
		}
		// basic HIT info that must be scraped off the page
		var obj = {
			hit_name:$("a.capsulelink[href='#']",$element).first().text().collapse_whitespace(),
			hit_id:$("a[href*='groupId']",$element).first().attr("href").match(/groupId=([A-Z0-9]{30})(?:&|$)/)[1],
			hit_desc:$("a[id*='description.tooltip']",$element).parent().next().text().collapse_whitespace(),
			hit_time:$("a[id*='duration_to_complete.tooltip']",$element).parent().next().text().collapse_whitespace(),
			hits_available:$("a[id*='number_of_hits.tooltip']",$element).parent().next().text().collapse_whitespace(),
			hit_reward:$("a[id*='reward.tooltip']",$element).parent().next().text().collapse_whitespace(),
			requester_name:$("a[href*='selectedSearchType=hitgroups']",$element).first().text().collapse_whitespace(),
			requester_id:$("a[href*='requesterId']",$element).first().attr("href").match(/requesterId=([A-Z0-9]{12,14})(?:&|$)/)[1]
		};
		// link properties for convenience, since these are long URLs that only use one bit of previously collected info
		obj.preview_link = ("https://www.mturk.com/mturk/preview?groupId="+obj.hit_id);
		obj.panda_link = ("https://www.mturk.com/mturk/previewandaccept?groupId="+obj.hit_id);
		obj.requester_hits = ("https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId="+obj.requester_id);
		obj.to_reviews = ("https://turkopticon.ucsd.edu/"+obj.requester_id);

		// parse qualifications
		var $qual_anchor = $("a[id*='qualificationsRequired.tooltip']",$element);
		if($qual_anchor.parent().next().text().trim() === "None") obj.quals = "None";
		else {
			var quals = [];
			$("tr:not(:first-of-type) td:first-of-type",$qual_anchor.closest("table")).each(function() {quals.push(highlight_qual($(this).text()));});
			obj.quals = quals.join("; ");
		}

		obj.author_url = "https://greasyfork.org/en/users/9665-mobius-evalon";
		obj.hermes_version = script_version;
		obj.hermes_url = "https://greasyfork.org/en/scripts/21175-hermes-hit-exporter";

		obj.date_time = new Date().toString();

		// by default the user has provided no completion time estimate, so this block needs to not be displayed until the user provides that info
		obj.pph_block = "";

		localStorage.hermes_hit = JSON.stringify(obj);
	}

	function get_template(t) {
		var format = (t || "vbulletin");
		return (localStorage.getItem("hermes_"+format+"_template") || default_template(format));
	}

	function default_template(format) {
		if(format === "vbulletin") return "[url={preview_link}][color=blue]{hit_name}[/color][/url] [[url={panda_link}][color=blue]PANDA[/color][/url]]\n"+
			"[b]Reward[/b]: {hit_reward}<pph_block:/{my_time} ({hourly_rate}/hour)>\n"+
			"[b]Time allowed[/b]: {hit_time}\n"+
			"[b]Available[/b]: {hits_available}\n"+
			"[b]Description[/b]: {hit_desc}\n"+
			"[b]Qualifications[/b]: {quals}\n\n"+
			"[b]Requester[/b]: [url={requester_hits}][color=blue]{requester_name}[/color][/url] [{requester_id}]\n"+
			"[url={to_reviews}][color=blue][b]Turkopticon[/b][/color][/url]: <to_block:[Pay: {to_pay}] [Fair: {to_fair}] [Fast: {to_fast}] [Comm: {to_comm}]>\n"+
			"[size=8px]Generated {date_time} with [url={hermes_url}][color=blue]Hermes HIT Exporter[/color][/url] {hermes_version} by [url={author_url}][color=blue]Mobius Evalon[/color][/url][/size]";
		else if(format === "markdown") return "[{hit_name}]({preview_link}) \\[[PANDA]({panda_link})\\]  \n"+
			"**Reward**: {hit_reward}<pph_block:/{my_time} ({hourly_rate}/hour)>  \n"+
			"**Time allowed**: {hit_time}  \n"+
			"**Available**: {hits_available}  \n"+
			"**Description**: {hit_desc}  \n"+
			"**Qualifications**: {quals}\n\n"+
			"**Requester**: [{requester_name}]({requester_hits}) \\[{requester_id}\\]  \n"+
			"[**Turkopticon**]({to_reviews}): <to_block:\\[Pay: {to_pay}\\] \\[Fair: {to_fair}\\] \\[Fast: {to_fast}\\] \\[Comm: {to_comm}\\]>  \n"+
			"^Generated {date_time} with [Hermes HIT Exporter]({hermes_url}) {hermes_version} by [Mobius Evalon]({author_url})";
	}

	function reset_interface()
	{
		$("#hhe_edit_template").hide();
		$("#hhe_export_output").hide();
		$("#hhe_mode_swap").text("Edit");
		$("#hhe_update_time").prop("disabled",true);
		$("#hhe_completion_time").val("");
		$("#hhe_export_format").prop("disabled",true).val(localStorage.hermes_export_format || "vbulletin");
	}

	function display_template() {
		var obj = localstorage_obj("hermes_hit"),
			template = get_template($("#hhe_export_format").val());
		$("#hhe_update_time, #hhe_export_format").prop("disabled",false);
		$.each(obj,function(key,val) {
			if(key.slice(-6) === "_block") template = template.replace(new RegExp(("<"+key+":.*?>"),"gi"),val);
			else template = template.replace(new RegExp(("\\{"+key+"\\}"),"gi"),val);
		});
		template = template.replace(/<.+?_block:(.*?)>/gi,"$1"); // remove unused override blocks
		$("#hhe_export_output").show().text(template);
	}

	function turkopticon() {
		var to_mirrors = ["https://mturk-api.istrack.in/multi-attrs.php?ids=",
						  "https://turkopticon.ucsd.edu/api/multi-attrs.php?ids="],
			hit_obj = localstorage_obj("hermes_hit");

		function mirror_domain(s) {
			return s.match(/^https{0,1}:\/\/(.+?)\//i)[1];
		}

		function to_request(url) {
			$.ajax({async:true,
					method:"GET",
					url:(url+hit_obj.requester_id)
				   })
				.fail(function() {
					console.log("Hermes HIT exporter: attempt to gather Turkopticon data from "+mirror_domain(url)+" mirror failed");
					var idx = (to_mirrors.indexOf(url)+1);
					if(idx < to_mirrors.length) {
						console.log("Hermes HIT exporter: attempting Turkopticon data request from mirror "+mirror_domain(to_mirrors[idx])+"...");
						to_request(to_mirrors[idx]);
					}
					else {
						hit_obj.to_block = "[Error]";
						console.log("Hermes HIT exporter: attempts to gather Turkopticon data from all available mirrors has failed");
					}
				})
				.done(function(response) {
					console.log("Hermes HIT exporter: successfully queried Turkopticon data from "+mirror_domain(url)+" mirror");
					var to_info = JSON.parse(response);
					if($.type(to_info) === "object" && to_info.hasOwnProperty(hit_obj.requester_id)) {
						if($.type(to_info[hit_obj.requester_id]) === "object") {
							hit_obj.to_pay = to_info[hit_obj.requester_id].attrs.pay;
							hit_obj.to_fair = to_info[hit_obj.requester_id].attrs.fair;
							hit_obj.to_fast = to_info[hit_obj.requester_id].attrs.fast;
							hit_obj.to_comm = to_info[hit_obj.requester_id].attrs.comm;
						}
						else {
							hit_obj.to_block = "[None]";
							console.log("Hermes HIT exporter: requester "+hit_obj.requester_name+" has no Turkopticon data");
						}
					}
					else console.log("Hermes HIT exporter: Turkopticon data returned from "+mirror_domain(url)+" mirror is malformed");
				})
				.always(function() {
					localStorage.hermes_hit = JSON.stringify(hit_obj);
					display_template();
				});
		}

		$("div#hermes_export_window textarea#hhe_export_output").show().text("Gathering Turkopticon data for "+hit_obj.requester_name+"...");
		to_request(to_mirrors[0]);
	}

	function json_obj(json) {
		var obj;
		if(typeof json === "string" && json.trim().length) {
			try {obj = JSON.parse(json);}
			catch(e) {console.log("Malformed JSON object.  Error message from JSON library: ["+e.message+"]");}
		}
		return obj;
	}

	function localstorage_obj(key) {
		var obj = json_obj(localStorage.getItem(key));
		if(typeof obj !== "object") localStorage.removeItem(key);
		return obj;
	}

	function zero_pad(a,l,d) {
		if($.type(a) !== "string") a = (""+a);
		var p = "";
		for(var i=a.length;i<l;i++) p += "0";
		if(d) a = (a+p);
		else a = (p+a);
		return a;
	}

	Date.prototype.toString = function() {
		return (""+this.getDate()+" "+this.getMonthString().slice(0,3)+" "+this.getFullYear()+", "+this.getTwoDigitHours()+":"+this.getTwoDigitMinutes()+"."+this.getTwoDigitSeconds());
	};

	Date.prototype.getMonthString = function() {
		switch(this.getMonth()) {
			case 0: return "January";
			case 1: return "February";
			case 2: return "March";
			case 3: return "April";
			case 4: return "May";
			case 5: return "June";
			case 6: return "July";
			case 7: return "August";
			case 8: return "September";
			case 9: return "October";
			case 10: return "November";
			case 11: return "December";
		}
	};

	Date.prototype.getTwoDigitHours = function() {
		return zero_pad(this.getHours(),2);
	};

	Date.prototype.getTwoDigitMinutes = function() {
		return zero_pad(this.getMinutes(),2);
	};

	Date.prototype.getTwoDigitSeconds = function() {
		return zero_pad(this.getSeconds(),2);
	};

	String.prototype.collapse_whitespace = function() {
		return this.replace(/\s+/g," ").trim();
	};

	String.prototype.contains_substring = function() {
		for(var i=0;i<arguments.length;i++) if(this.indexOf(arguments[i]) > -1) return true;
		return false;
	};

	$("head").append(
		$("<style/>")
			.attr("type","text/css")
			.text("#hermes_export_button {height: 16px; font-size: 10px; font-weight: bold; border: 1px solid; margin-left: 5px; padding: 0px 5px; background-color: transparent;} "+
				  "#hermes_export_window {position: fixed; left: 15vw; top: 10vh; background-color: #a5ccdd; border: 2px solid #5c9ebf; border-radius: 10px;} "+
			  	  "#hermes_export_window textarea {width: 400px; height: 250px; margin: 0px auto; display: block;} "+
			  	  "#hermes_export_window h1 {margin: 10px 0px; padding: 0px; font-size: 150%; font-weight: bold; text-align: center;} "+
			  	  "#hermes_export_window input#hhe_completion_time {width: 40px; text-align: center; margin: 0px 5px;} "+
			  	  "#hermes_export_window button#hhe_close {display: block; margin: 10px auto; clear: both;} "+
			  	  "#hermes_export_window select#hhe_export_format {margin: 0px 5px;} "+
			  	  "#hermes_export_window div.hhe_options div.left {display: inline-block; float: left; text-align: left;} "+
			  	  "#hermes_export_window div.hhe_options div.right {display: inline-block; float: right; text-align: right;} "+
			  	  "#hermes_export_window button {font: inherit;} "+
				  ".noscroll {overflow: hidden;} ")
	);
	$("body").append(
		$("<div/>")
			.attr("id","hermes_export_window")
			.append(
				$("<h1/>")
					.text("Hermes HIT exporter"),
				$("<textarea/>")
					.attr("id","hhe_export_output")
					.hide(),
				$("<textarea/>")
					.attr({
						"id":"hhe_edit_template",
						"title":"{hit_name}\n{hit_id}\n{hit_desc}\n{hit_time}\n{my_time}\n{hit_reward}\n{hourly_rate}\n{hits_available}\n{quals}\n{requester_id}\n{requester_name}\n"+
								"{preview_link}\n{panda_link}\n{requester_hits}\n{to_reviews}\n{to_pay}\n{to_fair}\n{to_fast}\n{to_comm}\n{hermes_url}\n{hermes_version}\n{author_url}"})
					.hide(),
				$("<div/>")
					.attr("class","hhe_options")
					.append(
						$("<div/>")
							.attr("class","left")
							.append(
								$("<select/>")
									.attr("id","hhe_export_format")
									.append(
										$("<option/>")
											.attr("value","vbulletin")
											.text("vBulletin"),
										$("<option/>")
											.attr("value","markdown")
											.text("Markdown")
									)
									.change(function() {
										localStorage.hermes_export_format = $(this).val();
										if($("#hhe_mode_swap").text() === "Edit") display_template();
									}),
								$("<button/>")
									.attr("id","hhe_mode_swap")
									.text("Edit")
									.click(function() {
										if($(this).text() === "Edit") {
											$("#hhe_edit_template").show().text(get_template($("#hhe_export_format").val()));
											$("#hhe_export_output").hide();
											$("#hhe_reset_template").show();
											$("#hhe_export_format, #hhe_update_time").prop("disabled",true);
											$(this).text("Done");
										}
										else {
											localStorage.setItem(("hermes_"+$("#hhe_export_format").val()+"_template"),$("#hhe_edit_template").val());
											$("#hhe_export_format, #hhe_update_time").prop("disabled",false);
											$("#hhe_edit_template, #hhe_reset_template").hide();
											$(this).text("Edit");
											display_template();
										}
									}),
								$("<button/>")
									.attr("id","hhe_reset_template")
									.text("Reset")
									.click(function() {
										if($("#hhe_mode_swap").text() === "Done" && confirm("Are you sure you want to reset the "+$("#hhe_export_format option:selected").text()+" template to default?\n\nThis can't be undone.")) {
											localStorage.removeItem("hermes_"+$("#hhe_export_format").val()+"_template");
											$("#hhe_edit_template").text(get_template($("#hhe_export_format").val()));
										}
									})
									.hide()
							),
						$("<div/>")
							.attr("class","right")
							.append(
								$("<label/>")
									.append(
										document.createTextNode("Time:"),
										$("<input/>")
											.attr({
												"type":"text",
									   			"id":"hhe_completion_time"
											})
							   		),
								$("<button/>")
									.text("Update")
									.attr("id","hhe_update_time")
									.click(function() {
										var hit = localstorage_obj("hermes_hit"),
											time = $("#hhe_completion_time").val(),
											mins = 0,
											secs = 0;

										if(time.indexOf(":") > -1) {
											mins = Math.floor(time.split(":")[0]*1);
											secs = Math.floor(time.split(":")[1]*1);
										}
										else {
											mins = Math.floor(time*1);
											secs = (((time*1)-mins)*60);
										}

										// in case some smart aleck enters something like 1:75
										while(secs > 59) {
											mins++;
											secs -= 60;
										}

										if((mins+secs) <= 0) {
											if(hit.hasOwnProperty("my_time")) delete hit.my_time;
											if(hit.hasOwnProperty("hourly_rate")) delete hit.hourly_rate;
											hit.pph_block = "";
										}
										else {
											hit.my_time = (""+mins+":"+zero_pad(secs,2));
											hit.hourly_rate = "$"+(((hit.hit_reward.slice(1)*1)/((mins*60)+secs))*3600).toFixed(2);
											if(hit.hasOwnProperty("pph_block")) delete hit.pph_block;
										}
										localStorage.hermes_hit = JSON.stringify(hit);
										display_template();
									})
							)
					),
				$("<button/>")
					.attr("id","hhe_close")
					.text("Close")
					.click(function() {
						$("#hermes_export_window").hide();
						$("body").removeClass("noscroll");
					})
			)
			.hide()
	);

	$("a.capsulelink").after(
		$("<button/>")
			.attr("id","hermes_export_button")
			.text("Export")
			.click(function() {
				var template = get_template();
				hit_info($(this).closest("table").parent().closest("table"));

				reset_interface();
				$("div#hermes_export_window").show();
				$("body").addClass("noscroll"); // so that quick scroll flicks won't scroll the page behind the export window, which is really annoying

				if(template.contains_substring("{to_pay}","{to_fair}","{to_fast}","{to_comm}")) turkopticon();
				else display_template();
			})
	);
});