Hermes HIT exporter

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();
			})
	);
});