Hermes HIT exporter

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

目前為 2016-07-11 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
			})
	);
});