· 6 years ago · Feb 06, 2020, 10:46 PM
1// ==UserScript==
2// @name TurkerViewJS
3// @namespace https://turkerview.com/mturk-scripts/
4// @version 10.9.3
5// @description Imports Turkerview.com functionality into mTurk.
6// @author ChrisTurk
7// @contrib Kadauchi - hijacked coloring functions that I then mangled, his were clean.
8// @contrib Slothbear - tons of bugfixes when I fail to test stuff in the real-world
9// @contrib SalemBeats - this jerk demands proper coding design structure.. pfft
10// @include /^http(s)?://worker\.mturk\.com/
11// @exclude https://worker.mturk.com/?finder_beta
12// @exclude https://worker.mturk.com/?hit_forker
13// @exclude https://worker.mturk.com/requesters/PandaCrazy/projects
14// @exclude https://worker.mturk.com/?filters[search_term]=pandacrazy=on
15// @grant GM_log
16// @require https://code.jquery.com/jquery-3.1.0.min.js
17// @require https://gist.github.com/raw/2625891/waitForKeyElements.js
18// @require https://use.fontawesome.com/fd61435f75.js
19// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.4/moment.min.js
20// @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.14/moment-timezone.min.js
21// @run-at document-end
22// ==/UserScript==
23
24
25let settings = {};
26
27var settingsDB;
28function initTVDB(){
29 const request = indexedDB.open(`turkerview`, 1);
30
31 request.onerror = function(event){
32 console.log(`TVJS could not open indexedDB`, event.target.errorCode);
33 }
34
35 request.onsuccess = function(event){
36 settingsDB = event.target.result;
37
38 var transaction = settingsDB.transaction([`turkerview`]);
39 var objectStore = transaction.objectStore(`turkerview`);
40 var request = objectStore.get(`settings`);
41
42 request.onsuccess = function(evented){
43 if (!request.result){
44 let check_old_api_key = localStorage.getItem(`turkerview_api_key`) || null;
45 var settings = {
46 key: `settings`,
47 api_key: check_old_api_key
48 }
49
50 var apiObjectStore = settingsDB.transaction(`turkerview`, `readwrite`).objectStore(`turkerview`);
51 apiObjectStore.add(settings);
52
53 initSetting();
54 return;
55 }
56
57 if (request.result['api_key'] != null) initSetting(request.result['api_key']);
58 else initSetting()
59 }
60 };
61
62 request.onupgradeneeded = function(event){
63 var db = event.target.result;
64 var objectStore = db.createObjectStore(`turkerview`, { keyPath: `key` });
65
66 objectStore.transaction.oncomplete = function(event){
67
68 let check_old_api_key = localStorage.getItem(`turkerview_api_key`) || null;
69 var settings = {
70 key: `settings`,
71 api_key: check_old_api_key
72 }
73
74 var apiObjectStore = db.transaction(`turkerview`, `readwrite`).objectStore(`turkerview`);
75 apiObjectStore.add(settings);
76 }
77
78 };
79}
80
81
82function initSetting(db_info = null){
83
84 ls_tv_api_key = localStorage.getItem('turkerview_api_key') || null;
85 settings = JSON.parse(localStorage.getItem('tv-settings')) || null;
86 if (settings === null){
87 settings = {
88 version: 2,
89 titlebar_wage_display: true,
90 display_requester_ratings: true,
91 display_hit_ratings: true,
92 disable_return_reviews: false,
93 show_return_favicon: true,
94 tv_api_key: (db_info != null) ? db_info : ls_tv_api_key,
95 return_warning_levels: {
96 underpaid: 'high',
97 broken: 'high',
98 screener: 'high',
99 tos: 'high',
100 writing: 'high',
101 downloads: 'high',
102 extraordinary_measures: 'high'
103 },
104 last_sync: null
105 }
106 commitSettings();
107 } else if (settings.version === undefined){
108 settings.version = 2;
109 settings.show_return_favicon = true;
110 settings.display_requester_ratings = true;
111 settings.display_hit_ratings = true;
112 settings.disable_return_reviews = false;
113 settings.tv_api_key = (db_info != null) ? db_info : ls_tv_api_key,
114 settings.return_warning_levels = {};
115 settings.return_warning_levels.underpaid = 'high';
116 settings.return_warning_levels.broken = 'high';
117 settings.return_warning_levels.screener = 'high';
118 settings.return_warning_levels.tos = 'high';
119 settings.return_warning_levels.writing = 'high';
120 settings.return_warning_levels.downloads = 'high';
121 settings.return_warning_levels.extraordinary_measures = 'high';
122 settings.last_sync = null;
123 } else if (db_info != null){
124 settings.tv_api_key = db_info
125 } else if (ls_tv_api_key != null){
126 settings.tv_api_key = ls_tv_api_key
127 }
128
129 ViewHeaders = new Headers([
130 ['X-VIEW-KEY', settings.tv_api_key],
131 ['X-APP-KEY', 'TurkerViewJS'],
132 ['X-APP-VER', ver] //SemVer
133 ]);
134
135 //settings are loaded, its safe to start the script
136 initTVJS();
137}
138
139function changeSettings(){
140 let the_api_key = $('input[name=tv_api_key]').val();
141 if (settings.tv_api_key != the_api_key && the_api_key != null && the_api_key != '' && the_api_key.length == 40){
142 var store_settings = {
143 key: `settings`,
144 api_key: the_api_key
145 }
146 var transaction = settingsDB.transaction([`turkerview`], `readwrite`);
147 var objectStore = transaction.objectStore(`turkerview`);
148 var request = objectStore.put(store_settings);
149
150 request.onsuccess = function(event){
151 //console.log('put settings:', request.result);
152 }
153
154 $('#api_connect').show(500);
155 ViewHeaders = new Headers([
156 ['X-VIEW-KEY', $('input[name=tv_api_key]').val()],
157 ['X-APP-KEY', 'TurkerViewJS'],
158 ['X-APP-VER', ver] //SemVer
159 ]);
160 }
161 settings.titlebar_wage_display = $('input[name=display_titlebar_wage]').is(':checked') ? true : false;
162 settings.show_return_favicon = $('input[name=show_return_favicon]').is(':checked') ? true : false;
163 settings.display_mturk_ratings = $('input[name=display_mturk_ratings]').is(':checked') ? true : false;
164 settings.display_requester_ratings = $('input[name=display_requester_ratings]').is(':checked') ? true : false;
165 settings.display_hit_ratings = $('input[name=display_hit_ratings]').is(':checked') ? true : false;
166 settings.enable_quick_reviews = $('input[name=enable_quick_reviews]').is(':checked') ? true : false;
167 settings.disable_return_reviews = $('input[name=disable_return_reviews]').is(':checked') ? true : false;
168 settings.tv_api_key = !$('input[name=tv_api_key]').val() ? null : $('input[name=tv_api_key]').val();
169 settings.return_warning_levels = {}
170 settings.exp_returners = $('input[name=exp_returners]').is(':checked') ? true : false;
171 settings.rr_screener_min = $('select[name=rr_screener_min]').val();
172 settings.return_warning_levels.underpaid = $('select[name=return_underpaid_warn_lvl]').val();
173 settings.return_warning_levels.broken = $('select[name=return_broken_warn_lvl]').val();
174 settings.return_warning_levels.screener = $('select[name=return_screener_warn_lvl]').val();
175 settings.return_warning_levels.tos = $('select[name=return_tos_warn_lvl]').val();
176 settings.return_warning_levels.writing = $('select[name=return_writing_warn_lvl]').val();
177 settings.return_warning_levels.downloads = $('select[name=return_downloads_warn_lvl]').val();
178 settings.return_warning_levels.extraordinary_measures = $('select[name=return_extraordinary_warn_lvl]').val();
179 settings.last_sync = settings.last_sync ? settings.last_sync : null;
180
181 commitSettings()
182}
183
184function commitSettings(){
185 localStorage.setItem('tv-settings', JSON.stringify(settings));
186 localStorage.setItem('ztv-settings', JSON.stringify(settings)); //duplicate for testing exporting
187 localStorage.setItem('turkerview_api_key', settings.tv_api_key);
188}
189
190let tvAgreement = (localStorage.getItem('tv-agree') == 'true') || false;
191let tvQualified = (localStorage.getItem('tv-qual-2') == 'true') ? true : (localStorage.getItem('tv-qual-2') == 'false') ? false : null;
192let hideReviewedFromTable = (localStorage.getItem('tv-hide-reviewed') == 'true') || false;
193let viewData = [];
194let hitData = [];
195let react = [];
196moment.tz.add("America/Los_Angeles|PST PDT PWT PPT|80 70 70 70|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261q0 1nX0 11B0 1nX0 SgN0 8x10 iy0 5Wp1 1VaX 3dA0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1a00 1fA0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|15e6");
197const today = moment.tz('America/Los_Angeles').format('YYYY-MM-DD');
198const queue = new RegExp('/worker\.mturk\.com\/tasks');
199const windowHREF = window.location.href;
200const ver = GM_info.scriptMetaStr.match(/version.*?(\d+.*)/)[1];
201
202let ViewHeaders = new Headers([
203 ['X-VIEW-KEY', settings.tv_api_key],
204 ['X-APP-KEY', 'TurkerViewJS'],
205 ['X-APP-VER', ver] //SemVer
206]);
207
208const hourlyFormat = (hourly) => {
209 let color = 'rgba(255, 0, 0, ';
210 if (hourly >= 10.00) { color = 'rgba(0, 128, 0, '; }
211 else if (hourly >= 7.25) { color = 'rgba(255, 165, 0, '; }
212 else if (hourly === '<i class="fa fa-minus"></i>') { color = 'rgba(128, 128, 128, '; }
213 return color;
214};
215const iconImage = (hourly) => {
216 let url = 'https://turkerview.com/assets/images/tv-unrated.png'
217
218 if (hourly == null) url = 'https://turkerview.com/assets/images/tv-unrated.png';
219 else if (hourly < 7.25) url = 'https://turkerview.com/assets/images/tv-red.png';
220 else if (hourly < 10.50) url = 'https://turkerview.com/assets/images/tv-orange.png';
221 else url = 'https://turkerview.com/assets/images/tv-green.png';
222
223 return url;
224};
225const confidence = (ratings_count) => {
226 let cOpacity = '0.5';
227 if (ratings_count >= 10) { cOpacity = '1'; }
228 else if (ratings_count >= 5) { cOpacity = '0.75'; }
229 else if (ratings_count >= 1) { cOpacity = '0.5'; }
230 return cOpacity;
231};
232const payFormat = (pay) => {
233 let payFA = '<i class="fa fa-thumbs-o-down" style="color: red;"></i> Very bad';
234 if (pay >= 4.25) { payFA = '<i class="fa fa-thumbs-o-up" style="color: green;"></i> Generous'; }
235 else if (pay >= 3.5) { payFA = '<i class="fa fa-thumbs-o-up" style="color: green;"></i> Good'; }
236 else if (pay >= 2.5) { payFA = '<i class="fa fa-handshake-o" style="color: orange;"></i> Fair'; }
237 else if (pay >= 1.5) { payFA = '<i class="fa fa-thumbs-o-down" style="color: red;"></i> Low'; }
238 else if (pay === null) { payFA = '<i class="fa fa-minus" style="color: grey;"></i> Unrated'; }
239 return payFA;
240};
241const classMap = total_reports => total_reports == 0 ? 'text-muted' :
242 total_reports < 3 ? 'text-warning' : 'text-danger';
243
244$(document).ready(function(){
245 initTVDB();
246});
247
248
249let syncing = false;
250function initTVJS(){
251 if (!settings.install_date) settings.install_date = moment.tz('America/Los_Angeles');
252 commitSettings();
253 if (tvQualified == null) checkQual('get');
254
255 if ($('ol.hit-set-table').length) {
256 if ($('.mturk-alert-content:contains(something is not right)').length > 0){
257 $('.mturk-alert-content:contains(something is not right)').append(`<p class="p-b-0 m-b-0" style="margin-top: 0.5rem;"><strong>If you just came from contacting a requester using TurkerView or a manual contact link you can ignore this warning, your message went through just fine!</strong></p>`)
258 }
259
260 waitForKeyElements ( '.requester-column:eq(0)', getRIDs);
261 }
262
263 let api_announcement = (!settings.tv_api_key || settings.tv_api_key === "" || settings.tv_api_key.length != 40) ? `<i class="fa fa-warning" style="color: #f39c12;"></i> ` : `<img src="https://turkerview.com/assets/images/tv-green.png" style="max-width: 16px; max-height: 16px; display: none;">`;
264 if ($('ul.nav.navbar-nav').length){
265 $('ul.nav.navbar-nav:first').find('.nav-item:last').after(`<li class="nav-item"><a id="nav-tv" class="nav-link" href="#">${api_announcement}TurkerView</a></li>`);
266 } else {
267 $('a:contains(HIT Details)')
268 $('a.navbar-brand').after(`<div class="navbar-divider hidden-xs-down"></div><ul class="nav navbar-nav hidden-xs-down"><li class="nav-item"><a id="nav-tv" class="nav-link" href="#">${api_announcement}TurkerView</a></li></ul>`);
269 }
270
271 $('body').on('click', '#nav-tv', function(){
272 //don't call the modal unless we need it
273 if (settings.last_sync){
274 let diff = moment.tz('America/Los_Angeles').diff(moment(settings.last_sync), 'seconds');
275
276 if (diff > 1800 && !/projects.*assignment_id/.test(windowHREF)){
277 if (diff > 129600) settings.install_date = moment.tz('America/Los_Angeles');
278 else cycleAASync();
279 }
280 }
281 if (!$('#tvDashModal').length){
282 initTurkerView();
283 initReviews();
284
285 $('input[name=display_titlebar_wage], input[name=display_mturk_ratings], input[name=display_requester_ratings], input[name=disable_return_reviews], input[name=display_hit_ratings], input[name=show_return_favicon], input[name=tv_api_key], input[name=exp_returners], select[name=rr_screener_min], select[name*=return_]').on('change keyup', function(){
286 changeSettings();
287 });
288
289 $('input[name=enable_quick_reviews]').on('change', function(){
290 changeSettings();
291 fillTable();
292 })
293
294 fillTable();
295 }
296 else {
297 $('#tv-table').find('tbody').html('');
298 fillTable();
299 $('body').addClass('global-modal-open modal-open');
300 $('#tvDashModal, #tv-dash-modal-backdrop').show();
301
302 }
303 });
304
305 if (/https:\/\/worker\.mturk\.com\/tasks/.test(windowHREF)){
306 getRIDs();
307 } else if (/https:\/\/worker\.mturk\.com\/contact_requester/.test(windowHREF)){
308 contactPage();
309 } else if (/projects.*assignment_id/.test(windowHREF)){
310 trackingTask();
311 } else if (/projects.*\/tasks/.test(windowHREF)){
312 let assignmentData = $('.project-detail-bar').find('[data-react-class="require(\'reactComponents/common/ShowModal\')[\'default\']"]').data('react-props').modalOptions;
313 let assignableHitsCount = assignmentData.assignableHitsCount;
314 let hit_set_id = document.querySelectorAll('form[action*="projects/')[0].action.match(/projects\/([A-Z0-9]+)\/tasks/)[1];
315 let rid = assignmentData['contactRequesterUrl'].match(/requester_id%5D=(.*?)&/)[1];
316 let title = assignmentData['projectTitle'];
317 let reward = assignmentData['monetaryReward']['amountInDollars'];
318 getHitReturnData(hit_set_id, assignableHitsCount);
319 getDetailedHitData(rid, reward, title, assignableHitsCount, hit_set_id);
320 } else if (/dashboard/.test(windowHREF)){
321
322 /* Let's get some stats */
323 let surveyWorkTime = 0;
324 let surveyPE = 0;
325 let batchWorkTime = 0;
326 let batchPE = 0;
327 let totalPE = 0;
328 let trackedWorkTime = 0;
329 let requesterBreakdownTableHtml = ``;
330 Object.keys(localStorage)
331 .forEach(function(key){
332 if (/^tv_/.test(key)) {
333 let json = JSON.parse(localStorage.getItem(key));
334
335 if (json['date'] != today) return;
336
337 let reviewButton = ``;
338 if (json['reviewId'] == null){
339 reviewButton = `<a class="btn btn-primary btn-sm btn-review" data-toggle="tooltip" data-title="Leave a review on TurkerView!" data-hitKey="${escape(key)}">Review</a>`;
340 } else if (json['reviewId'] != null){
341 reviewButton = `<a href="https://turkerview.com/reviews/edit.php?id=${json['reviewId']}" target="_blank" class="btn btn-default btn-sm" data-hitKey="${escape(key)}" data-toggle="tooltip" data-title="Edit Review (Takes you to TurkerView)">Edit</a>`;
342 }
343
344 if (json['times'].length == 1){
345 surveyWorkTime += json['completionTime'];
346 surveyPE += json['reward'];
347 trackedWorkTime += json['completionTime'];
348
349 requesterBreakdownTableHtml += `
350<div class="col-xs-4" style="overflow: hidden; white-space: nowrap;">${json['requester']}</div>
351<div class="col-xs-2 text-xs-right">${json['times'].length}</div>
352<div class="col-xs-2 text-xs-right">${moment().startOf('day').seconds((json['completionTime']/1000)).format('H:mm:ss')}</div>
353<div class="col-xs-2 text-xs-right">$${((3600/(json['completionTime']/1000))*json['reward']).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}/hr</div>
354<div class="col-xs-2 text-xs-right">$${json['reward'].toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</div>
355<div class="col-xs-1" style="display: none;">${reviewButton}</div>`;
356
357 return;
358 }
359
360 /* Use Median Values on Batches */
361 let mind;
362 let arr = json['times'];
363 mind = Math.floor(median(filterOutliers(arr))/1000);
364 if (!mind) mind = Math.floor(median(arr)/1000);
365
366 //batchWorkTime += mind*(arr.length);
367 batchPE += json['reward']*(arr.length);
368
369 let batch_work_time = 0;
370 let batch_earned = 0;
371 json['times'].forEach(time => {
372 batchWorkTime += time/1000;
373 trackedWorkTime += time;
374 batch_work_time += time;
375 batch_earned += json['reward'];
376 });
377
378 requesterBreakdownTableHtml += `
379<div class="col-xs-4" style="overflow: hidden; white-space: nowrap;">${json['requester']}</div>
380<div class="col-xs-2 text-xs-right">${json['times'].length}</div>
381<div class="col-xs-2 text-xs-right">${moment().startOf('day').seconds((batch_work_time/1000)).format('H:mm:ss')}</div>
382<div class="col-xs-2 text-xs-right">$${((3600/(batch_work_time/1000))*batch_earned).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}/hr</div>
383<div class="col-xs-2 text-xs-right">$${batch_earned.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</div>
384<div class="col-xs-1" style="display: none;">${reviewButton}</div>`;
385
386 /* Calculate the hourly based on every single individual HIT */
387 /* We're not going to use this for now, I think overall it'll be less accurate since wild swings in completion times will throw things off
388 json['times'].forEach(function(time){
389 workTime += time;
390 pe += json['reward'];
391 });
392 */
393 }
394 });
395
396 totalPE = surveyPE+batchPE;
397
398 let overall_wages = ((3600/(trackedWorkTime/1000))*totalPE).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
399 overall_wages = Number(overall_wages) ? `$${overall_wages}/hr` : `-`;
400
401 let trackedFormatted = moment().startOf('day').seconds(Math.floor(trackedWorkTime/1000)).format('H:mm:ss');
402
403 let daily_survey_hourly = ((3600/(surveyWorkTime/1000))*surveyPE).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
404 daily_survey_hourly = Number(daily_survey_hourly) ? `$${daily_survey_hourly}/hr` : `-`;
405 let daily_batch_hourly = ((3600/(batchWorkTime))*batchPE).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
406 daily_batch_hourly = Number(daily_batch_hourly) ? `$${daily_batch_hourly}/hr` : `-`;
407 if ($('.row.m-b-sm:contains(Projected Earnings)').length){
408 //Dashboard Enhancer present, just append
409 $('.row.m-b-sm:contains(Projected Earnings)').after(`
410<div class="row m-b-sm">
411 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Tracked Time</strong></div>
412 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${trackedFormatted}</div>
413</div>
414<div class="row m-b-sm">
415 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Overall Wages</strong></div>
416 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${overall_wages.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</div>
417</div>
418<div class="row m-b-sm">
419 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Batch Wages</strong></div>
420 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${daily_batch_hourly}</div>
421</div>
422<div class="row m-b-sm">
423 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Survey Wages</strong></div>
424 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${daily_survey_hourly}</div>
425</div>`);
426 } else{
427
428 /* Rebuild the entire PE dash >.> Y U NO HAVE DASH ENHANCER??? */
429 $('.col-xs-12.col-md-4.col-md-push-8').prepend(`
430<div class="row m-b-xl">
431 <div class="col-xs-12">
432 <h2 class="m-b-md">Today's Activity</h2>
433 <div class="row">
434 <div class="col-xs-12">
435 <div class="border-gray-lightest p-a-sm">
436 <div class="row m-b-sm">
437 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Tracked Earnings</strong></div>
438 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">$${totalPE.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</div>
439 </div>
440 <div class="row m-b-sm">
441 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Tracked Time</strong></div>
442 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${trackedFormatted}</div>
443 </div>
444 <div class="row m-b-sm">
445 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Overall Wages</strong></div>
446 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${overall_wages.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</div>
447 </div>
448 <div class="row m-b-sm">
449 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Batch Wages</strong></div>
450 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${daily_batch_hourly}</div>
451 </div>
452 <div class="row m-b-sm">
453 <div class="col-xs-7 col-sm-6 col-lg-7"><strong>Survey Wages</strong></div>
454 <div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">${daily_survey_hourly}</div>
455 </div>
456 <div class="row m-b-sm">
457 <div class="col-xs-7 col-sm-6 col-lg-7">
458 <span class="expand-requester-breakdown-button">
459 <a class="table-expand-collapse-button" href="#" style="display: block; text-align: center;"><i class="fa fa-plus-circle"></i>
460 <span class="button-text">Show Requesters</span>
461 </a>
462 </span>
463 </div>
464 <div class="col-xs-5 col-sm-6 col-lg-5">
465 <span class="collapse-requester-breakdown-button">
466 <a class="table-expand-collapse-button" href="#" style="display: block; text-align: center;"><i class="fa fa-minus-circle"></i>
467 <span class="button-text">Hide Requesters</span>
468 </a>
469 </span>
470 </div>
471 </div>
472 <!-- Requester Breakdown Table -->
473 <div id="requester-breakdown" class="row m-b-sm" style="display: none;">
474 <div class="col-xs-4"><strong>Requester</strong></div>
475 <div class="col-xs-2 text-xs-right"><strong>Submitted</strong></div>
476 <div class="col-xs-2 text-xs-right"><strong>Time</strong></div>
477 <div class="col-xs-2 text-xs-right"><strong>Wages</strong></div>
478 <div class="col-xs-2 text-xs-right"><strong>Earned</strong></div>
479 <div class="col-xs-1 text-xs-right" style="display: none;"><strong>Review</strong></div>
480 ${requesterBreakdownTableHtml}
481 </div>
482
483
484 </div>
485 </div>
486 </div>
487 </div>
488</div>
489`)
490 $('.expand-requester-breakdown-button').click(function(){
491 $('#requester-breakdown').slideDown(250);
492 });
493 $('.collapse-requester-breakdown-button').click(function(){
494 $('#requester-breakdown').slideUp(250);
495 });
496
497 /*
498 initTurkerView();
499 initReviews();
500
501 $('input[name=display_titlebar_wage], input[name=display_requester_ratings], input[name=disable_return_reviews], input[name=display_hit_ratings], input[name=show_return_favicon], input[name=tv_api_key], select[name*=return_]').on('change keyup', function(){
502 changeSettings();
503 });
504
505 fillTable();
506 */
507
508 }
509
510 $('.result-count-info').append(`<span id="tv-aa-sync" style="cursor: pointer; margin-left: 5px;"><i class="fa fa-refresh"></i> Sync TV Review Approvals</span>`);
511
512 $('#tv-aa-sync').click(function(){
513 if (syncing) return;
514 syncing = true;
515 $('#tv-aa-sync').addClass('text-muted').html(`<i class="fa fa-refresh fa-spin"></i> Syncing`);
516 cycleAASync();
517 })
518
519 setInterval(dashWatcher, 900000);
520
521 function dashWatcher(){
522 let diff = moment.tz('America/Los_Angeles').diff(moment(settings.last_sync), 'seconds');
523
524 if (diff > 7200 || !settings.last_sync){
525 if (diff > 129600) {
526 settings.install_date = moment.tz('America/Los_Angeles');
527 settings.last_sync = moment.tz('America/Los_Angeles');
528 commitSettings();
529 cycleAASync();
530 }
531 else cycleAASync();
532 }
533 }
534
535 let diff = moment.tz('America/Los_Angeles').diff(moment(settings.last_sync), 'seconds');
536
537 if (diff > 900 || !settings.last_sync){
538 if (diff > 129600) {
539 settings.install_date = moment.tz('America/Los_Angeles');
540 settings.last_sync = moment.tz('America/Los_Angeles');
541 commitSettings();
542 cycleAASync();
543 }
544 else cycleAASync();
545
546
547 } //else console.log('we wont send another hit status request for '+(900 - diff)+' seconds..');
548
549 $(document).on('click', '.btn-update-rejection', function(){
550 $(this).attr('disabled', true);
551 let json = JSON.parse(localStorage.getItem($(this).attr('id')));
552
553 let postObject = {
554 fast_rating_5: [], fast_rating_4: [], fast_rating_3: [], fast_rating_2: [], fast_rating_1: [],
555 rejection_ids: [{ review_id: json.reviewId, requester_feedback: json.feedback, requester_name: json.requester }]
556 }
557 let postData = new FormData();
558 postData.append('data', JSON.stringify(postObject));
559
560 fetch(`https://turkerview.com/api/v2/reviews/update/status/`, {
561 method: 'POST',
562 body: postData,
563 headers: ViewHeaders
564 }).then(res => {
565 if (!res.ok) throw res;
566
567 return res.json()
568 }).then(res => {
569 if ($('#rejection-thanks').length == 0) $(this).closest('table').before(`<p>Thank you!</p><p>Thank you for sharing your experience with this requester. If you need help getting the rejections overturned please drop by the current <a href='https://forum.turkerview.com/forums/daily-mturk-hits-threads.2/' target='_blank'>Daily Thread</a></p>`);
570 }).catch(ex => {
571 console.log(ex);
572 alert(`Sorry, TVJS couldn't communicate your rejection to TV! Let CT know we saw this exception: ${ex} - also available in console (F12)`)
573 });
574 });
575
576 checkQual('react');
577 if (settings.tv_api_key == null || settings.tv_api_key == '' || settings.tv_api_key.length != 40){
578 $('h1:contains(Overview)').before(tvApiAlert());
579 }
580 }
581}
582
583let lastDay = '';
584function cycleAASync(){
585 if (!settings.tv_api_key || settings.tv_api_key.length != 40) {
586 settings.last_sync = moment.tz('America/Los_Angeles');
587 syncing = false;
588 commitSettings();
589 return;
590 }
591
592 let newtoday = moment(today).tz('America/Los_Angeles', true);
593 let installdate = moment(settings.install_date).tz('America/Los_Angeles', true);
594 let installDiff = newtoday.startOf('day').diff(installdate.startOf('day'), 'days');
595
596 let datesWithReviews = [];
597
598 Object.keys(localStorage)
599 .forEach(function(key){
600 if (/^tv_/.test(key)) {
601 let json = JSON.parse(localStorage.getItem(key));
602
603 let sizeEst = (JSON.stringify(json).length*16)/(8*1024);
604 /* Let's clean up old data or data we no longer have use for so we can keep localstorage clean for the user */
605 /*
606 Single record HITs are <400 len, ~.75kb in size so we could store up to 6,000 without overflowing localstorage, we should never get close to that.
607 A massive Forker / Overwatch / etc install complicates this immensely, so lets be stingy with what TV is storing to avoid any possible complications while still getting good AA data
608 */
609 let days = moment(today).tz('America/Los_Angeles', true).diff(moment(json['date']).tz('America/Los_Angeles', true), 'days');
610
611 if (days > 7 && !json.reviewId){
612 //This record is too old to be reviewed & doesn't have a review id, its safe to remove it
613 localStorage.removeItem(key);
614 return;
615 } else if (days > 3 && json.reviewId && ( (json.hit_status == 1 && json.task_count == 1) || json.approved == json.task_count ) ){
616
617 //This record is too old to be edited from TVJS, it has been reviewed & we already uploaded the approval time no need to keep it
618 localStorage.removeItem(key);
619 return;
620 } else if (days > 10 && json.reviewId && sizeEst > 5){
621 //This record is simply too big to keep longer than 10 days, this should be incredibly rare to invoke, but adds a very important safety net around the user's limited local storage length
622 localStorage.removeItem(key);
623 return;
624 } else if (days > 4 && !json.times){
625 //This record is from prior to TVJS10, we should remove it
626 localStorage.removeItem(key);
627 return;
628 } else if (days > 31){
629 //Its been too long, lets move on with our lives.
630 localStorage.removeItem(key);
631 return;
632 }
633
634
635 if (json.hit_stauts == 1 && json.task_count == 1) return; //filter out hits we already know the AA time of. don't filter rejections, we need to know if they overturn
636 if (json.fast && json.approved == json.task_count) return; //only count approvals here, if there are rejections we should check to see if they overturn
637
638 if (!datesWithReviews.includes(json['date'])) datesWithReviews.push(json['date']);
639 }
640 });
641
642 datesWithReviews.sort(function(a,b){return new Date(a).getTime() - new Date(b).getTime()}).reverse();
643
644 let yesterday = moment(today).tz('America/Los_Angeles', true).subtract(1, 'days').format('YYYY-MM-DD');
645 let startSubtract = 1;
646 let startCooldown = 0;
647 let installWait = -1;
648 let days_scanned = 0;
649
650 /* we'll always check the last two days IF(!) there are reviews, just in case */
651 if (datesWithReviews.includes(today)) {
652 days_scanned++;
653 startCooldown += 1500;
654 checkHitStatus(today, 50);
655 lastDay = today;
656 }
657
658 if (datesWithReviews.includes(yesterday)) {
659 days_scanned++;
660 startCooldown += 1500;
661 setTimeout(function(){ checkHitStatus(yesterday); }, startCooldown);
662 lastDay = yesterday
663 }
664
665
666
667 for (let i = 0; i < 32; i++){
668 startSubtract++;
669 installWait++;
670
671 let dateFormatted = moment(today).tz('America/Los_Angeles', true).subtract(startSubtract, 'days').format('YYYY-MM-DD');
672 if (!datesWithReviews.includes(dateFormatted)) continue;
673
674 days_scanned++;
675 lastDay = dateFormatted;
676 startCooldown += 1500;
677 setTimeout(function(){ checkHitStatus(dateFormatted); }, startCooldown);
678 }
679
680 if (days_scanned == 0){
681 syncing = false;
682 if ($('#tv-aa-sync').hasClass('text-muted')) $('#tv-aa-sync').removeClass('text-muted').html(`<i class="fa fa-refresh"></i> Done!`);
683 settings.last_sync = moment.tz('America/Los_Angeles');
684 commitSettings();
685 }
686
687 if (datesWithReviews.length === 0){
688 console.log('no reviews');
689 syncing = false;
690 settings.last_sync = moment.tz('America/Los_Angeles');
691 commitSettings();
692 $('#tv-aa-sync').removeClass('text-muted').html(`<i class="fa fa-refresh"></i> Done!`);
693 }
694}
695
696async function checkHitStatus(date, page_limit = 50){
697//console.log('checking...',date);
698 let statusHits = [];
699 let paidPages = 1;
700 let approvedPages = 1;
701 let rejectedPages = 1;
702 let ps = [];
703
704
705 const workHistory = await Promise.all([
706 fetch(`https://worker.mturk.com/status_details/${date}?utf8=✓&assignment_state=Approved&format=json`).then(res => res.json()).then(res => {
707 approvedPages = Math.ceil(res.total_num_results/20);
708 res.results.forEach(hit => {
709 statusHits.push(hit)
710 });
711 for (i = 2; i < page_limit; i++){
712 if (i > approvedPages) break;
713 let x = fetch(`https://worker.mturk.com/status_details/${date}?utf8=✓&assignment_state=Approved&format=json&page_number=${i}`).then(res => res.json()).then(res => {
714 res.results.forEach(hit => {
715 statusHits.push(hit)
716 })
717 });
718 ps.push(x)
719 }
720 }),
721 fetch(`https://worker.mturk.com/status_details/${date}?utf8=✓&assignment_state=Paid&format=json`).then(res => res.json()).then(res => {
722 paidPages = Math.ceil(res.total_num_results/20);
723 res.results.forEach(hit => {
724 statusHits.push(hit)
725 });
726 for (i = 2; i < page_limit; i++){
727 if (i > paidPages) break;
728 let x = fetch(`https://worker.mturk.com/status_details/${date}?utf8=✓&assignment_state=Paid&format=json&page_number=${i}`).then(res => res.json()).then(res => {
729 res.results.forEach(hit => {
730 statusHits.push(hit)
731 })
732 });
733 ps.push(x)
734 }
735 }),
736 fetch(`https://worker.mturk.com/status_details/${date}?utf8=✓&assignment_state=Rejected&format=json`).then(res => res.json()).then(res => {
737 rejectedPages = Math.ceil(res.total_num_results/20);
738 res.results.forEach(hit => {
739 statusHits.push(hit)
740 });
741 for (i = 2; i < page_limit; i++){
742 if (i > rejectedPages) break;
743 let x = fetch(`https://worker.mturk.com/status_details/${date}?utf8=✓&assignment_state=Rejected&format=json&page_number=${i}`).then(res => res.json()).then(res => {
744 res.results.forEach(hit => {
745 statusHits.push(hit)
746 })
747 });
748 ps.push(x)
749 }
750 })
751 ]);
752
753 const pageHistory = await Promise.all(ps);
754
755 let diff = moment(today).diff(date, 'days');
756
757 let newtoday = moment(today).tz('America/Los_Angeles', true);
758
759 let installdate = moment(settings.install_date).tz('America/Los_Angeles', true);
760 let installDiff = newtoday.startOf('day').diff(installdate.startOf('day'), 'days');
761
762 let submitday = moment(date).tz('America/Los_angeles', true);
763 let submitDiff = newtoday.startOf('day').diff(submitday.startOf('day'), 'days'); //# of days since hit was submitted
764
765 let syncDate = moment(settings.last_sync).tz('America/Los_Angeles', true);
766 let syncDiff = newtoday.startOf('day').diff(syncDate.startOf('day'), 'days'); //# of days since last sync
767
768 //console.log(`Installed: ${installDiff} days ago, submitted: ${submitDiff} days ago, synced ${syncDiff} days ago`);
769
770 /* Rules:
771 1) Install diff should be less than days since HIT was submitted (if we submit a HIT 7 days ago, but only installed for 2 days, we don't know when it approved, don't mark fast)
772 2) Sync diff should be less than 1,5 days to reliably track fast approval
773 3) Use checkDiff to mark fast according to the old rules
774 */
775
776 let str = `tv_${date}`;
777 let keyTest = new RegExp("^"+str);
778 // 5 == good (~1day AA), 1 == bad (15-30 day)
779 let fast_rating_5 = [];
780 let fast_rating_4 = [];
781 let fast_rating_3 = [];
782 let fast_rating_2 = [];
783 let fast_rating_1 = [];
784 let approved_ids = [];
785 let rejected_reviews = [];
786 let new_rejections = [];
787 let new_overturned_rejections = [];
788 let temp_object = {fast5: [], fast4: [], fast3: [], fast2: [], fast1: []};
789
790 let batches = [];
791
792 Object.keys(localStorage).forEach(function(key){
793 if (keyTest.test(key)) {
794 let json = JSON.parse(localStorage.getItem(key));
795
796 /* The only thing this does is upload a copy of a record we already have, we only do this if we're not confident our server will process the original request, look to remove. */
797 if (json.reviewId && json.fast){
798 if (json.fast == 5) temp_object.fast5.push(json.reviewId);
799 else if (json.fast == 4) temp_object.fast4.push(json.reviewId);
800 else if (json.fast == 3) temp_object.fast3.push(json.reviewId);
801 else if (json.fast == 2) temp_object.fast2.push(json.reviewId);
802 else if (json.fast == 1) temp_object.fast1.push(json.reviewId);
803
804 return; //don't need to look for this, we've already marked it for AA
805 }
806
807 //we need to keep track of:
808 // total task_count that we're scraping, as well as the max we should get to for a particular review
809 //when task_count == 1 make sure the assignment_id matches up
810
811 statusHits.forEach(function(hitResult){
812 /* We use this since status pages don't store group_id but we still group reviews by that string, ugh */
813 if (hitResult.requester_id != json.rid) return;
814 if (hitResult.title != json.title) return;
815 if (hitResult.reward.amount_in_dollars != json.reward) return;
816
817
818 /* Process Batch Records */
819 if (json.task_count > 1 && json.assignment_id == hitResult.assignment_id){
820 //we want to handle these differently, we'll update the approved/rejected count and if it equals the total submitted we'll let TV know we're done w/ this record
821 let t_approved = 0;
822 let t_rejected = 0;
823 let rejection_feedback = [];
824 let one_feedback = null;
825
826 statusHits.forEach(function(hit){
827 //loop through them all, see if we can get an approved/rejected count == task_count [ie, no pending left]
828 if (hit.requester_id != json.rid) return;
829 if (hit.title != json.title) return;
830 if (hit.reward.amount_in_dollars != json.reward) return;
831
832 if (hit.state == "Rejected") {
833 t_rejected++;
834 json.feedback = hit.requester_feedback;
835 if (!rejection_feedback.includes(hit.requester_feedback)) rejection_feedback.push(hit.requester_feedback);
836 one_feedback = hit.requester_feedback;
837 }
838 else if (hit.state == "Approved" || hit.state == "Paid") t_approved++;
839
840 });
841
842
843 //if (t_rejected > 0 && t_approved > 0) json.hit_status = null;
844 if (json.approved >= 1 && !json.fast){
845 if (installDiff <= submitDiff && syncDiff <= 2 && submitDiff < 2) {
846 json.fast = 5;
847 }
848 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 4) {
849 json.fast = 4;
850 }
851 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 8) {
852 json.fast = 3;
853 }
854 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 15) {
855 json.fast = 2;
856 }
857 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 32) {
858 json.fast = 1;
859 }
860 }
861
862 let mind;
863 /* Batch results*/
864 if (json['multi'] == true && json['times']){
865 let arr = json['times'];
866 mind = Math.floor(median(filterOutliers(arr))/1000);
867 if (!mind) mind = Math.floor(median(arr)/1000);
868 } else mind = Math.floor(json['completionTime'] / 1000);
869
870 if ((t_approved + t_rejected) >= json.task_count && (t_approved != json.approved && t_rejected == 0) ){
871 //the rejections might overturn, so we'll need to keep syncing this
872
873 json.task_count = (t_approved + t_rejected);
874 json.approved = t_approved;
875 json.rejected = t_rejected;
876
877 let record = {
878 requester_id: json.rid,
879 task_count: json.task_count,
880 approved: json.approved,
881 rejected: json.rejected,
882 feedback: one_feedback,
883 tracked_completion: mind,
884 fast: json.fast ? json.fast : null,
885 review_id: json.reviewId
886 };
887
888 if (json.reviewId) batches.push(record);
889 }
890
891
892 localStorage.setItem(key, JSON.stringify(json));
893 return;
894 }
895 /* End Batch Processing */
896
897 /* For one-off HITs, since we're matching title/rid/reward, we need to check the assignment_id here to make sure it's correct */
898 /* This prevents issues with requesters who use the same title/etc for two different surveys [Andy's $5 HITs w/ multiple in one day] */
899 if (hitResult.assignment_id != json.assignment_id) return; //make sure we only update the correct record for 1-off HITs
900
901 //if the hit_status is already set, don't overwrite it.
902 if (json.fast) return;
903 if (json.fast) return; //we've already handled this, should never get here tbh
904 if (json.hit_status == -1 && hitResult.state == "Rejected") return; //we know this is rejected, only way we'll do anything more w/ it is if it overturns
905 if (json.hit_status == 1) return; //we've already marked this as approved
906
907
908 if (hitResult.state == "Rejected"){
909 //change the state to rejected, then return so we don't update _fast rating
910 json.hit_status = -1;
911 json.feedback = hitResult.requester_feedback;
912 if (json.reviewId) rejected_reviews.push(json.reviewId);
913 localStorage.setItem(key, JSON.stringify(json));
914 if(json.reviewId) new_rejections.push({key: key, date: json.date, requester_name: json.requester, tasks: json.task_count, title: json.title, reviewId: json.reviewId, feedback: json.feedback})
915 return;
916 }
917
918 /* This was previously marked as rejected & is no longer rejected [we would have returned earlier if it was], safe to mark as overturned */
919 if (json.hit_status == -1) {
920 if(json.reviewId) new_overturned_rejections.push({key: key, date: json.date, requester_name: json.requester, tasks: json.task_count, title: json.title, reviewId: json.reviewId, feedback: json.feedback})
921 }
922
923 /* We know this is approved for sure, we only retrieve approved/paid/rejected results from the status pages */
924 json.hit_status = 1;
925
926 if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff < 2) {
927 json.fast = 5;
928 if (json.reviewId) fast_rating_5.push(json.reviewId);
929 }
930 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 4) {
931 json.fast = 4;
932 if (json.reviewId) fast_rating_4.push(json.reviewId);
933 }
934 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 8) {
935 json.fast = 3;
936 if (json.reviewId) fast_rating_3.push(json.reviewId);
937 }
938 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 15) {
939 json.fast = 2;
940 if (json.reviewId) fast_rating_2.push(json.reviewId);
941 }
942 else if (installDiff >= submitDiff && syncDiff <= 2 && submitDiff <= 32) {
943 json.fast = 1;
944 if (json.reviewId) fast_rating_1.push(json.reviewId);
945 } else {
946 //we don't know the AA time, just upload the approval info
947 if (json.reviewId) approved_ids.push(json.reviewId);
948 }
949
950 localStorage.setItem(key, JSON.stringify(json));
951
952 })
953 }
954 });
955
956
957 if (batches.length > 0) {
958
959 let batch_uploads = {
960 batches: batches
961 };
962 let batchData = new FormData();
963 batchData.append('data', JSON.stringify(batch_uploads));
964
965
966 fetch(`https://turkerview.com/api/v2/reviews/update/batch/`, {
967 method: 'POST',
968 body: batchData,
969 headers: ViewHeaders
970 }).then(res => {
971 if (!res.ok) throw res;
972 //console.log(res);
973 return res.json();
974 }).then(res => {
975 //we actually don't need to do anything with this, relic from testing
976 //console.log(res);
977 }).catch(ex => {
978 console.log('ex: '+ex);
979 });
980 }
981
982 if (fast_rating_5.length > 0 || fast_rating_4.length > 0 || fast_rating_3.length > 0 || fast_rating_2.length > 0 || fast_rating_1.length > 0 || approved_ids.length > 0){
983 let postObject = {
984 fast_rating_5: fast_rating_5,
985 fast_rating_4: fast_rating_4,
986 fast_rating_3: fast_rating_3,
987 fast_rating_2: fast_rating_2,
988 fast_rating_1: fast_rating_1,
989 approved_ids: approved_ids
990 }
991
992 postObject.fast_rating_5 = postObject.fast_rating_5.concat(temp_object.fast5);
993 postObject.fast_rating_4 = postObject.fast_rating_4.concat(temp_object.fast4);
994 postObject.fast_rating_3 = postObject.fast_rating_3.concat(temp_object.fast3);
995 postObject.fast_rating_2 = postObject.fast_rating_2.concat(temp_object.fast2);
996 postObject.fast_rating_1 = postObject.fast_rating_1.concat(temp_object.fast1);
997
998 let postData = new FormData();
999 postData.append('data', JSON.stringify(postObject));
1000
1001 fetch(`https://turkerview.com/api/v2/reviews/update/status/`, {
1002 method: 'POST',
1003 body: postData,
1004 headers: ViewHeaders
1005 }).then(res => {
1006 if (!res.ok) throw res;
1007 //console.log(res);
1008 return res.json();
1009 }).then(res => {
1010 //we actually don't need to do anything with this, relic from testing
1011 //console.log(res);
1012 }).catch(ex => {
1013 console.log('ex: '+ex);
1014 });
1015
1016 }
1017
1018 if(new_rejections.length > 0) {
1019 let divTable = '';
1020 new_rejections.forEach(rejection => {
1021 divTable += `
1022<tr>
1023 <td class="text-xs-right col-xs-1"><a href="https://worker.mturk.com/status_details/${rejection['date']}?utf8=✓&assignment_state=Rejected" target="_blank">${rejection['date']}</a></td>
1024 <td class="text-xs-right col-xs-3">${rejection['requester_name']}</td>
1025 <td class="text-xs-right col-xs-6">${rejection['title']}</td>
1026 <td class="text-xs-right col-xs-1">${rejection['tasks']}</td>
1027 <td class="text-xs-right col-xs-1"><button type="button" id="${rejection['key']}" class="btn btn-danger btn-sm btn-update-rejection">Update</button></td>
1028</tr>
1029<tr><td><strong>Reason: </strong>${rejection['feedback']}</td></tr>`;
1030 });
1031 $('#MainContent').prepend(`
1032<div class="alert alert-danger">
1033 <h4>TurkerView has detected new rejections!</h4>
1034 <table style="width: 100%; margin-top: 8px;">
1035 <thead>
1036 <tr>
1037 <th class="text-xs-right col-xs-1">Date</th>
1038 <th class="text-xs-right col-xs-3">Requester</th>
1039 <th class="text-xs-right col-xs-6">HIT Title</th>
1040 <th class="text-xs-right col-xs-1">Submitted</th>
1041 <th class="text-xs-right col-xs-1">Review</th>
1042 </tr>
1043 </thead>
1044 <tbody>
1045 ${divTable}
1046 </tbody>
1047 </table>
1048</div>`);
1049 }
1050
1051 if (new_overturned_rejections.length > 0){
1052 let divTable = '';
1053 new_overturned_rejections.forEach(rejection => {
1054 divTable += `
1055<tr>
1056 <td class="text-xs-right col-xs-1"><a href="https://worker.mturk.com/status_details/${rejection['date']}?utf8=✓&assignment_state=Rejected" target="_blank">${rejection['date']}</a></td>
1057 <td class="text-xs-right col-xs-3">${rejection['requester_name']}</td>
1058 <td class="text-xs-right col-xs-6">${rejection['title']}</td>
1059 <td class="text-xs-right col-xs-1">${rejection['tasks']}</td>
1060 <td class="text-xs-right col-xs-1"><a href="https://turkerview.com/reviews/edit.php?id=${rejection['reviewId']}" target="_blank" class="btn btn-success btn-sm">Edit</a></td>
1061</tr>
1062<tr><td><strong>Reason: </strong>${rejection['feedback']}</td></tr>`;
1063 });
1064 $('#MainContent').prepend(`
1065<div class="alert alert-success">
1066 <h4>TurkerView has detected new overturned rejections!</h4>
1067 <p>We updated the status of the HIT in your review, if you have anything else to add please visit the website :)</p>
1068 <table style="width: 100%; margin-top: 8px;">
1069 <thead>
1070 <tr>
1071 <th class="text-xs-right col-xs-1">Date</th>
1072 <th class="text-xs-right col-xs-3">Requester</th>
1073 <th class="text-xs-right col-xs-6">HIT Title</th>
1074 <th class="text-xs-right col-xs-1">Submitted</th>
1075 <th class="text-xs-right col-xs-1">Review</th>
1076 </tr>
1077 </thead>
1078 <tbody>
1079 ${divTable}
1080 </tbody>
1081 </table>
1082</div>`);
1083 }
1084
1085 if (settings){
1086 settings.last_sync = moment.tz('America/Los_Angeles');
1087 commitSettings();
1088 }
1089 if (date == lastDay) {
1090 syncing = false;
1091 $('#tv-aa-sync').removeClass('text-muted').html(`<i class="fa fa-refresh"></i> Done!`);
1092 }
1093}
1094
1095function checkQual(method){
1096 localStorage.removeItem('tv-qual');
1097 switch (method){
1098 case 'get':
1099 $.get('https://worker.mturk.com/dashboard?format=json').done(function(json){
1100
1101 let approved = json['hits_overview']['approved'];
1102 let approval_rate = json['hits_overview']['approval_rate'];
1103
1104 let rejection_impact = {
1105 approved: json['hits_overview']['approved'],
1106 approval_rate: json['hits_overview']['approval_rate'],
1107 pending: json['hits_overview']['pending'],
1108 rejected: json['hits_overview']['rejected']
1109 }
1110
1111 //localStorage.setItem('tv-rejection-object', JSON.stringify(rejection_impact)); //on second thought, we don't need this skip & come back to it if that changes for rejection warnings
1112 localStorage.setItem('tv-app-range', closestApprovalTotal(approved));
1113 localStorage.setItem('tv-app-rate', closestApprovalPercentage(approval_rate/100));
1114 tvQualified = true;
1115 localStorage.setItem('tv-qual-2', true);
1116 });
1117 break;
1118 case 'react':
1119 let approved = parseInt($('#dashboard-hits-overview').find('div.row:contains(Approved):first').children('div:not(:contains(Approved))').text().replace(/,/g, ''));
1120 let approval_rate = parseFloat($('#dashboard-hits-overview').find('div.row:contains(Approval Rate):first').children('div:not(:contains(Approval Rate))').text().replace(/%/g, ''));
1121
1122 localStorage.setItem('tv-app-range', closestApprovalTotal(approved));
1123 localStorage.setItem('tv-app-rate', closestApprovalPercentage(approval_rate/100));
1124 tvQualified = true;
1125 localStorage.setItem('tv-qual-2', true);
1126 break;
1127 }
1128
1129}
1130
1131function closestApprovalTotal(num) {
1132 var arr = [100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000];
1133 var curr = arr[0];
1134 var diff = Math.abs (num - curr);
1135 for (var val = 0; val < arr.length; val++) {
1136 var newdiff = Math.abs (num - arr[val]);
1137 if (newdiff < diff) {
1138 diff = newdiff;
1139 curr = arr[val];
1140 }
1141 }
1142 return curr;
1143}
1144
1145function closestApprovalPercentage(num) {
1146 var arr = [0.5, 0.75, 0.85, 0.90, 0.95, 0.98, 0.99, 0.995, 1];
1147 var curr = arr[0];
1148 var diff = Math.abs (num - curr);
1149 for (var val = 0; val < arr.length; val++) {
1150 var newdiff = Math.abs (num - arr[val]);
1151 if (newdiff < diff) {
1152 diff = newdiff;
1153 curr = arr[val];
1154 }
1155 }
1156 return curr;
1157}
1158
1159var pauseDT;
1160var pausedElapsedTime = 0;
1161var paused = false;
1162var hourlyTracker;
1163
1164function trackingTask(){
1165
1166 let userActive = false;
1167 //window.addEventListener('mousemove', checkWindowFocus);
1168 //$(window).on('mousemove keydown', checkWindowFocus);
1169
1170 function checkWindowFocus() {
1171 userActive = true;
1172 //console.log('user is in the window');
1173 window.removeEventListener('mousemove', checkWindowFocus);
1174 $(window).off('mousemove keydown');
1175 let now = moment.tz('America/Los_Angeles');
1176 pausedElapsedTime += moment(now).diff(moment(then));
1177 hourlyTracker = setInterval(hourlyDoHicky, 2500 );
1178 }
1179
1180
1181 //accepted hit work page, need to log the opening time + submission time
1182 let returned = 0;
1183 let assignmentData = $('.project-detail-bar').find('[data-react-class="require(\'reactComponents/common/ShowModal\')[\'default\']"]').data('react-props').modalOptions;
1184 let assignableHitsCount = assignmentData.assignableHitsCount;
1185 let requester = assignmentData['requesterName'];
1186 let rid = assignmentData['contactRequesterUrl'].match(/requester_id%5D=(.*?)&/)[1];
1187 let title = assignmentData['projectTitle'];
1188 let reward = assignmentData['monetaryReward']['amountInDollars'];
1189 let hit_set_id = document.querySelectorAll('form[action*="projects/')[0].action.match(/projects\/([A-Z0-9]+)\/tasks/)[1];
1190 let hitKey = 'tv_'+today+"_"+hit_set_id;
1191 let assignment_id = window.location.href.match(/assignment_id=([A-Z0-9]+)/)[1];
1192
1193 let then = moment.tz('America/Los_Angeles');
1194
1195 $('.detail-bar-label:contains(Requester)').next().append(`[ <a href="https://turkerview.com/requesters/${rid}-${slugify(requester)}" target="_blank">TV Profile</a> ]`);
1196
1197 $('form[action*="rtrn"]').submit(function(e){
1198 returned = 1;
1199 });
1200
1201 //$('div.row.h4').children('.col-xs-7').append(`<div id="hourlyContainer" class="pull-right" style="display: inline; text-align: center !important; opacity: 0.65;"><span id="hourlyPause" title="This will pause the timer for your hourly wage, use this when taking a break or otherwise not working on the HIT." class="hidden-xs-down pull-right" data-toggle="tooltip" data-placement="bottom" style="cursor: pointer; position: sticky;">Pause Hourly</span><span id="tvHourlyValue" class="hidden-xs-down pull-right" style="margin-right: 5px;">...</span></div>`);
1202
1203 let new_amt_goal = $('div.row.h4').children().length == 3 ? true : false;
1204
1205 if (new_amt_goal){
1206 //new goal bars
1207 let hits_completed = $('.daily-progress-bar:eq(0)').text().replace(/ \/ /, '').trim();
1208 let hits_goal = $('.worker-history-goal-input:eq(0)').attr('placeholder');
1209
1210 let reward_earned = $('.daily-progress-bar:eq(1)').text().replace(/ \/ /, '').trim();
1211 let reward_goal = $('.worker-history-goal-input:eq(1)').attr('placeholder');
1212
1213 $('div.row.h4').children('.col-md-6').html(``);
1214 $('div.row.h4').children('.col-md-6').append(`<span id="hijacked-goals" class="hidden-xs-down" style="opacity: 0.65">${hits_completed} / ${hits_goal} HITs Completed <span style="padding-right: 8px;"></span> ${reward_earned} / ${reward_goal} Reward Earned </span><div id="hourlyContainer" class="pull-right" style="display: inline; text-align: center !important; opacity: 0.65;"><span id="hourlyPause" title="This will pause the timer for your hourly wage, use this when taking a break or otherwise not working on the HIT." class="hidden-xs-down pull-right" data-toggle="tooltip" data-placement="bottom" style="cursor: pointer; position: sticky;">Pause Hourly</span><span id="tvHourlyValue" class="hidden-xs-down pull-right" style="margin-right: 5px;">...</span></div>`);
1215 } else{
1216 $('div.row.h4').children('.col-xs-7').append(`<div id="hourlyContainer" class="pull-right" style="display: inline; text-align: center !important; opacity: 0.65;"><span id="hourlyPause" title="This will pause the timer for your hourly wage, use this when taking a break or otherwise not working on the HIT." class="hidden-xs-down pull-right" data-toggle="tooltip" data-placement="bottom" style="cursor: pointer; position: sticky;">Pause Hourly</span><span id="tvHourlyValue" class="hidden-xs-down pull-right" style="margin-right: 5px;">...</span></div>`);
1217 }
1218
1219
1220
1221
1222 if ($('iframe').length > 0){
1223 $('iframe').before(`<div id="hourlySticky" class="h4" style="position: fixed; top: 56px; right: 50px; z-index: 9999; display: inline; float: right;"></div>`);
1224 }else{
1225 //no iframe HIT
1226 $('body').before(`<div id="hourlySticky" class="h4" style="position: fixed; top: 56px; right: 50px; z-index: 9999; display: inline; float: right;"></div>`);
1227 }
1228
1229
1230 let position = 0;
1231 let iframePositionTop = $('iframe').length > 0 ? $('iframe').offset().top : 80;
1232 $(window).on('scroll', function(){
1233 var doc = document.documentElement;
1234 var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
1235
1236 if (top > 20 && position == 0){
1237 let moveThis = $('#hourlyContainer').detach();
1238 $('#hourlySticky').prepend(moveThis);
1239 position = 1;
1240 } else if (top < 20 && position == 1) {
1241 let moveThis = $('#hourlyContainer').detach();
1242 if (new_amt_goal) $('div.row.h4').children('.col-md-6').append(moveThis);
1243 else $('div.row.h4').children('.col-xs-7').append(moveThis);
1244 position = 0;
1245 }
1246 if (top > 20 && top < iframePositionTop){
1247 let pixelOffset = iframePositionTop - top;
1248 $('#hourlySticky').css('top', (pixelOffset+26)+'px');
1249 } else if (top > 20){
1250 $('#hourlySticky').css('top', '56px');
1251 }
1252 });
1253
1254 $('iframe').on('load', function(){
1255 //if we want to move to frame-load hourly tracking use this
1256 });
1257
1258 $(document).on('click', '#hourlyPause', function(){
1259 if (paused == false){
1260 clearInterval(hourlyTracker);
1261 paused = true;
1262 pauseDT = moment.tz('America/Los_Angeles');
1263 let currentPausedTime = pausedElapsedTime;
1264
1265 let now = moment.tz('America/Los_Angeles');
1266 let interval = moment(now).diff(moment(then));
1267
1268 if (reward != 0.00 && reward != '0.00'){
1269 let currentHourlyWage = (3600/((interval - currentPausedTime)/1000))*reward;
1270 $('#tvHourlyValue').text(`$${currentHourlyWage.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}/hr`);
1271 if (currentHourlyWage >= 12.50) $('#tvHourlyValue').removeClass('text-danger, text-warning').addClass('text-success');
1272 else if (currentHourlyWage >= 7.25) $('#tvHourlyValue').removeClass('text-danger, text-success').addClass('text-warning');
1273 else $('#tvHourlyValue').removeClass('text-success, text-warning').addClass('text-danger');
1274 }
1275
1276 let total_sec = (interval - currentPausedTime)/1000;
1277 let min = Math.floor(total_sec/60);
1278 let sec = (total_sec%60).toFixed(0);
1279 $('#hourlyPause').text(`Unpause Hourly [${min < 10 ? '0' : ''}${min}:${sec < 10 ? '0' : ''}${sec}]`).css('color', 'red');
1280 } else{
1281 paused = false;
1282 setInterval(hourlyDoHicky, 1000);
1283 let now = moment.tz('America/Los_Angeles');
1284 pausedElapsedTime += moment(now).diff(moment(pauseDT));
1285
1286 let currentPausedTime = pausedElapsedTime;
1287 let interval = moment(now).diff(moment(then));
1288
1289 let total_sec = (interval - currentPausedTime)/1000;
1290 let min = Math.floor(total_sec/60);
1291 let sec = (total_sec%60).toFixed(0);
1292 $('#hourlyPause').text(`Pause Hourly [${min < 10 ? '0' : ''}${min}:${sec < 10 ? '0' : ''}${sec}]`).css('color', '');
1293 }
1294 });
1295
1296 let badHourly = false;
1297 let original_document_title = document.title;
1298
1299 hourlyTracker = setInterval(hourlyDoHicky, 1000);
1300
1301 function hourlyDoHicky(){
1302 let currentPausedTime = pausedElapsedTime;
1303 //update hourly every so often
1304 if (paused) return;
1305 //if (!userActive) return;
1306
1307 let now = moment.tz('America/Los_Angeles');
1308 let interval = moment(now).diff(moment(then));
1309 let docTitle = '';
1310 if (reward != 0.00 && reward != '0.00'){
1311 let currentHourlyWage = (3600/((interval - currentPausedTime)/1000))*reward;
1312 $('#tvHourlyValue').text(`$${currentHourlyWage.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}/hr`);
1313 docTitle = `$${currentHourlyWage.toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0})}/hr`;
1314 if (currentHourlyWage >= 12.50) $('#tvHourlyValue').removeClass('text-danger, text-warning').addClass('text-success');
1315 else if (currentHourlyWage >= 7.25) $('#tvHourlyValue').removeClass('text-danger, text-success').addClass('text-warning');
1316 else $('#tvHourlyValue').removeClass('text-success, text-warning').addClass('text-danger');
1317
1318 if (currentHourlyWage <= 7.25 & !badHourly && !localStorage.getItem('mts-return-reviews')){
1319 badHourly = true;
1320 if ($('.me-bar').find('#hourlyPause').length == 0) $('#hourlyContainer').append(`<br><a class="text-warning tv-return-warn" style="margin-right: 5px; cursor: pointer;">[ Review & Return ]</a>`);
1321 else $('#hourlyContainer').prepend(`<a class="text-warning tv-return-warn pull-right" style="margin-right: 5px; cursor: pointer;">[ Review & Return ]</a>`);
1322 }
1323 }
1324 let total_sec = (interval - currentPausedTime)/1000;
1325 let min = Math.floor(total_sec/60);
1326 let sec = (total_sec%60).toFixed(0);
1327 $('#hourlyPause').text(`Pause Hourly [${min < 10 ? '0' : ''}${min}:${sec < 10 ? '0' : ''}${sec}]`);
1328 if (settings.titlebar_wage_display) document.title = docTitle + ` [${min < 10 ? '0' : ''}${min}:${sec < 10 ? '0' : ''}${sec}] ` + original_document_title;
1329 }
1330
1331 $(window).on('message', receiveMessage);
1332
1333 function receiveMessage(e) {
1334 let msg = JSON.stringify(e.originalEvent.data);
1335 if (msg.indexOf("assignmentId") == -1) return;
1336 //we can parse the assignment id from the message if it becomes risky but for now lets just trust worker not to post messages other than submission.. lol
1337 //let message = JSON.parse(e.originalEvent.data); //we can do message['assignmentId'] or something here if need be.
1338
1339 let now = moment.tz('America/Los_Angeles');
1340 let ms = moment(now).diff(moment(then));
1341 if (paused){
1342 pausedElapsedTime += moment(now).diff(moment(pauseDT));
1343 }
1344 ms = ms - pausedElapsedTime;
1345
1346 let submittedObject = {
1347 date: today,
1348 requester: requester,
1349 rid: rid,
1350 title: title,
1351 hit_set_id: hit_set_id,
1352 task_count: 1,
1353 reward: reward,
1354 completionTime: ms,
1355 times: [ms],
1356 assignment_id: assignment_id,//we'll only use this for rejection overturn requests, its not uploaded w/ normal reviews & isn't available for upload yet
1357 submitTime: now,
1358 multi: false,
1359 reviewed: false,
1360 reviewId: null
1361 };
1362 if (!localStorage.getItem(hitKey)){
1363 localStorage.setItem(hitKey, JSON.stringify(submittedObject));
1364 } else{
1365 let previousRecord = JSON.parse(localStorage.getItem(hitKey));
1366 previousRecord['multi'] = true;
1367 previousRecord["task_count"]++;
1368 previousRecord['times'].push(ms);
1369 localStorage.setItem(hitKey, JSON.stringify(previousRecord));
1370 }
1371 }
1372
1373 if ($('iframe').length == 0){
1374 //no iframe, rebuild the TV object when user clicks submit
1375 $('div[data-react-class*=SubmitAssignedTaskSubmissionForm]').find('.btn.btn-primary').click(function(){
1376 let now = moment.tz('America/Los_Angeles');
1377 let ms = moment(now).diff(moment(then));
1378 if (paused){
1379 pausedElapsedTime += moment(now).diff(moment(pauseDT));
1380 }
1381 ms = ms - pausedElapsedTime;
1382
1383 let submittedObject = {
1384 date: today,
1385 requester: requester,
1386 rid: rid,
1387 title: title,
1388 hit_set_id: hit_set_id,
1389 task_count: 1,
1390 reward: reward,
1391 completionTime: ms,
1392 times: [ms],
1393 assignment_id: assignment_id,//we'll only use this for rejection overturn requests, its not uploaded w/ normal reviews & isn't available for upload yet
1394 submitTime: now,
1395 multi: false,
1396 reviewed: false,
1397 reviewId: null
1398 };
1399 if (!localStorage.getItem(hitKey)){
1400 localStorage.setItem(hitKey, JSON.stringify(submittedObject));
1401 } else{
1402 let previousRecord = JSON.parse(localStorage.getItem(hitKey));
1403 previousRecord['multi'] = true;
1404 previousRecord["task_count"]++;
1405 previousRecord['times'].push(ms);
1406 localStorage.setItem(hitKey, JSON.stringify(previousRecord));
1407 }
1408 });
1409 }
1410
1411 if (settings.disable_return_reviews) return;
1412 if (localStorage.getItem('mts-return-reviews')) return; //lets start deferring to mts for things.
1413
1414 let tvReturnModal = `
1415<div class="modal fade in" id="tvReturnModal" style="display: none;">
1416 <div id="tv-return-dialog" class="modal-dialog">
1417 <div class="modal-content">
1418 <div class="modal-header">
1419 <button id="tv-return-modal-close" type="button" class="close" data-dismiss="modal"><svg class="close-icon" data-reactid=".8.0.0.0.0.0"><g fill="none" stroke="#555" class="close-icon-graphic" stroke-width="2.117" stroke-linecap="round" data-reactid=".8.0.0.0.0.0.0"><path d="M1.2 1.3l7.4 7.5M1.2 8.8l7.4-7.5" data-reactid=".8.0.0.0.0.0.0.0"></path></g></svg></button>
1420 <h2 class="modal-title">Submit a TurkerView Return Report</h2>
1421 </div>
1422 <div class="modal-body">
1423 <div id="return-review-alert" class="alert alert-dismissable alert-success hide">
1424 <button type="button" class="close" data-dismiss="alert">×</button>
1425 <h4>Thank you!</h4>
1426 <p>Your review for has been added!</p>
1427 </div>
1428 <form id="tv-return-review" action="https://turkerview.com/api/v2/returns/submit/" method="POST">
1429 <script>
1430 let minCommentLength = 20;
1431
1432 $('input[name=underpaid]').change(function(){
1433 if ($(this).is(':checked')) $('#progressest').show();
1434 else {
1435 $('#progressest').hide();
1436 $('#progressSelect').prop('selectedIndex', 0);
1437 $('#estVal').text('');
1438 }
1439 });
1440
1441 $('#progressSelect').change(function(){
1442 let estimated_progress = $(this).val();
1443 if (estimated_progress == "Unsure") {
1444 $('#estVal').text('');
1445 return;
1446 }
1447 let reward = $('input[name=reward]').val();
1448 let elapsed_work_time = $('input[name=elapsed_work_time]').val();
1449 let estimated_completion_time = elapsed_work_time/estimated_progress;
1450 let ect = estimated_completion_time/1000;
1451 let min = Math.floor(ect/60);
1452 let sec = Math.floor(ect%60);
1453 let estimated_hourly = (3600/(estimated_completion_time/1000)) * reward;
1454 $('#estVal').text(min + ":"+sec + "s / $" + (estimated_hourly).toFixed(2) + "/hr");
1455 });
1456
1457 $('input[name=tos]').change(function(){
1458 if ($(this).is(':checked')) $('#tos_toggle').show();
1459 else {
1460 $('#tos_toggle').hide();
1461 $('input[name=tos_type]').prop('checked', false)
1462 }
1463 });
1464
1465 $('input[name=writing]').change(function(){
1466 if ($(this).is(':checked')) $('#writing_toggle').show();
1467 else {
1468 $('#writing_toggle').hide();
1469 $('input[name=writing_type]').prop('checked', false)
1470 }
1471 });
1472
1473 $('input[name=downloads]').change(function(){
1474 if ($(this).is(':checked')) $('#downloads_toggle').show();
1475 else {
1476 $('#downloads_toggle').hide();
1477 $('input[name=downloads_type]').prop('checked', false)
1478 }
1479 });
1480
1481 $('input[name=em]').change(function(){
1482 if ($(this).is(':checked')) $('#em_toggle').show();
1483 else {
1484 $('#em_toggle').hide();
1485 $('input[name=em_type]').prop('checked', false)
1486 }
1487 });
1488
1489 $('textarea[name=comment]').on("input", function(){
1490 let currentLength = $(this).val().length;
1491 $('#comment-length').text('Please be descriptive so other workers can better understand your review, currently at ' + currentLength + '/' + minCommentLength +' minimum characters');
1492
1493 });
1494
1495 $('#tv-return-review').find('input').change(function(){
1496 if ($('input[name=tos_type][value=9]').is(':checked') ||
1497 $('input[name=writing_type][value=9]').is(':checked') ||
1498 $('input[name=downloads_type][value=9]').is(':checked') ||
1499 $('input[name=em_type][value=9]').is(':checked') ||
1500 $('input[name=underpaid]').is(':checked') ||
1501 $('input[name=broken]').is(':checked')
1502 ) {
1503
1504 $('textarea[name=comment]').attr('minlength', minCommentLength).prop('required', true);
1505 $('#comment-required, #comment-length').show();
1506 return;
1507 }
1508
1509 $('textarea[name=comment]').removeAttr('minlength').removeAttr('required');
1510 $('#comment-required, #comment-length').hide();
1511
1512 });
1513
1514 </script>
1515 <div class="row" style="margin-bottom: .7rem;">
1516 <div class="col-xs-12 text-muted">
1517 <h3 id="return-wage-est" style="text-align: center;"></h3>
1518 </div>
1519 </div>
1520 <div class="row">
1521 <h2>Reason for returning?</h2>
1522 <div class="col-xs-12 text-muted form-group">
1523 <label><input type="checkbox" name="underpaid"> Underpaid</label><small> - use this if the HIT isn't worth the time involved to finish it.</small>
1524 <p id="progressest" style="display: none; padding-left: 30px;">Progress Estimate:
1525 <select name="progressSelect" id="progressSelect">
1526 <option>Unsure</option>
1527 <option value=".075">5-10% Complete</option>
1528 <option value=".175">10-25% Complete</option>
1529 <option value=".375">25-50% Complete</option>
1530 <option value=".625">50-75% Complete</option>
1531 <option value=".875">75-100% Complete</option>
1532 </select>
1533 <span id="estVal"></span>
1534 </p>
1535 </div>
1536
1537 <div class="col-xs-12 text-muted form-group">
1538 <label><input type="checkbox" name="broken"> Broken</label><small> - the HIT cannot be completed - do NOT use this for no survey code.</small>
1539 </div>
1540
1541 <div class="col-xs-12 text-muted form-group">
1542 <label><input type="checkbox" name="unpaid_screener"> Unpaid Screener</label><small> - use this if you were screened out without compensation.</small>
1543 </div>
1544
1545 <div class="col-xs-12 text-muted form-group">
1546 <label><input type="checkbox" name="tos"> ToS Violation</label>
1547 <div id="tos_toggle" style="padding-left: 30px; display: none;">
1548 <label style="display: block;"><input type="radio" value="1" name="tos_type"> Personally Identifiable Information (PII) Minor <small>(Email, Zip, Company Name)</small></label>
1549 <label style="display: block;"><input type="radio" value="2" name="tos_type"> Personally Identifiable Information (PII) Major <small>(Full Name, Phone #, SSN)</small></label>
1550 <label style="display: block;"><input type="radio" value="3" name="tos_type"> SEO/Referral/Review Fraud</label>
1551 <label style="display: block;"><input type="radio" value="4" name="tos_type"> Phishing/Malicious Activity</label>
1552 <label style="display: block;"><input type="radio" value="9" name="tos_type"> Misc/Other</label>
1553 </div>
1554
1555 </div>
1556
1557 <div class="col-xs-12 text-muted form-group">
1558 <label><input type="checkbox" name="writing"> Writing</label><small> - use if this HIT requires annoying "write about a time when" prompts.</small>
1559 <div id="writing_toggle" style="padding-left: 30px; display: none;">
1560 <label style="display: block;"><input type="radio" value="1" name="writing_type"> Experiential Writing <small>"Write about a time when..."</small></label>
1561 <label style="display: block;"><input type="radio" value="9" name="writing_type"> Misc/Other</label>
1562 <p style="font-size: 85%; margin-left: 20px;">While Misc/Other requires writing usually copying the writing prompt is enough to get the point across to other users (please try not to disclose survey content, though!)</p>
1563 </div>
1564 </div>
1565
1566 <div class="col-xs-12 text-muted form-group">
1567 <label><input type="checkbox" name="downloads"> Downloads / Installs</label>
1568 <div id="downloads_toggle" style="padding-left: 30px; display: none;">
1569 <label style="display: block;"><input type="radio" value="1" name="downloads_type"> Inquisit <small> - use if this HIT utilizes the unpopular Inquisit plugin.</small></label>
1570 <label style="display: block;"><input type="radio" value="2" name="downloads_type"> Browser Extension</label>
1571 <label style="display: block;"><input type="radio" value="3" name="downloads_type"> Phone/Tablet Apps</label>
1572 <label style="display: block;"><input type="radio" value="9" name="downloads_type"> Misc/Other</label>
1573 </div>
1574 </div>
1575
1576 <div class="col-xs-12 text-muted form-group">
1577 <label><input type="checkbox" name="em"> Extraordinary Measures</label>
1578 <div id="em_toggle" style="padding-left: 30px; display: none;">
1579 <label style="display: block;"><input type="radio" value="1" name="em_type"> Phone Calls</label>
1580 <label style="display: block;"><input type="radio" value="2" name="em_type"> Webcam/Face requirements</label>
1581 <label style="display: block;"><input type="radio" value="9" name="em_type"> Misc/Other</label>
1582 </div>
1583 </div>
1584
1585 </div>
1586 <div class="row">
1587 <div class="col-xs-12 text-muted">
1588 <h2>Comment</h2>
1589 <textarea name="comment" rows="4" style="width: 100%;" placeholder="Leave a comment..."></textarea>
1590 <span id="comment-required" style="display: none;" class="text-danger">* Required<small class="text-muted"> - Please leave at least a short note with your review to help other workers avoid the same problem.</small></span>
1591 <p id="comment-length" style="display: none;" class="text-muted"></p>
1592 </div>
1593 </div>
1594 <input type="hidden" name="group_id" value="${hit_set_id}">
1595 <input type="hidden" name="requester_id" value="${rid}">
1596 <input type="hidden" name="reward" value="${reward}">
1597 <input type="hidden" name="elapsed_work_time">
1598 <input type="hidden" name="version" value="${ver}">
1599 <input type="hidden" name="app_range" value="${localStorage.getItem('tv-app-range') || null}">
1600 <input type="hidden" name="app_rate" value="${localStorage.getItem('tv-app-rate') || null}">
1601 <div class="row" style="margin-bottom: 0">
1602 <div class="col-xs-12 text-muted">
1603 <button type="submit" class="btn btn-primary pull-right">Submit Data & Return HIT</button>
1604 </div>
1605 </div>
1606 </form>
1607 </div>
1608 </div>
1609 </div>
1610</div>
1611<div id="tv-return-modal-backdrop" class="modal-backdrop fade in" style="display: none;"></div>`;
1612
1613 $('footer').after(tvReturnModal);
1614
1615 $('#tv-return-modal-backdrop, #tv-return-modal-close, #tvReturnModal').on('click', function(){
1616 $('#tv-return-modal-backdrop, #tvReturnModal').hide();
1617 $('body').removeClass('global-modal-open modal-open');
1618 });
1619
1620 $('#tv-return-dialog').click(function(e){
1621 e.stopPropagation();
1622 });
1623
1624 let inputForm = document.getElementById(`tv-return-review`);
1625
1626 inputForm.addEventListener(`submit`, function(event){
1627 event.preventDefault();
1628
1629 $('#tv-return-review').find('button[type=submit]').attr('disabled', true).prepend('<i class="fa fa-spinner fa-pulse"></i> ');
1630 var formData = new FormData(inputForm);
1631
1632 fetch(inputForm.action, {
1633 method: `POST`,
1634 body: formData,
1635 headers: ViewHeaders
1636 }).then(response => {
1637 if (!response.ok) throw response
1638
1639 return response.json();
1640 }).then(response => {
1641
1642 if (response.class != 'success') $('#tv-return-review').find('button[type=submit]').removeAttr('disabled').find('i').remove();
1643 const noticeAlert = document.getElementById(`return-review-alert`);
1644 noticeAlert.classList.remove(`alert-success`);
1645 noticeAlert.classList.remove(`alert-warning`);
1646 noticeAlert.classList.remove(`alert-danger`);
1647 noticeAlert.classList.add(`alert-${response.class}`);
1648 noticeAlert.classList.remove(`hide`);
1649 noticeAlert.innerHTML = response.html;
1650
1651 if (response.status == 'success'){
1652 setTimeout(function(){
1653 let form = document.querySelectorAll(`form[action*="rtrn"]`)[0].submit();
1654 }, 250)
1655 } else inputForm.querySelector(`button[type=submit]`).disabled = false;
1656
1657 }).catch(exception => {
1658 console.log(exception);
1659 });
1660
1661 });
1662
1663
1664 /*
1665 $('#tv-return-review').submit(function(e){
1666 e.preventDefault();
1667
1668 $('#tv-return-review').find('button[type=submit]').attr('disabled', true).prepend('<i class="fa fa-spinner fa-pulse"></i> ');
1669 var url = $(this).attr('action');
1670 $.ajax({
1671 type: 'POST',
1672 url: url,
1673 data: $(this).serialize(),
1674 xhrFields: {
1675 withCredentials: true
1676 },
1677 dataType: 'text',
1678 success: function(data){
1679 if (/Your review was added/.test(data)){
1680 $('#return-review-alert').removeClass('alert-success alert-warning alert-danger hide').addClass('alert-success');
1681 $('#return-review-alert').find('h4').text('Thank you!');
1682 $('#return-review-alert').find('p').text('For letting other workers know this wasnnt a good HIT!');
1683 $('form[action*="rtrn"]').submit();
1684 } else{
1685 //$('#return-review-failure').removeClass('hide');
1686 //if (data.indexOf('Duplicate entry') > -1) $('#return-review-failure').children('p').text("You've already returned & reviewed this HIT.");
1687 //else $('#return-review-failure').children('p').html(data);
1688
1689 $('#return-review-alert').removeClass('alert-success alert-warning alert-danger hide').addClass('alert-danger');
1690 $('#return-review-alert').find('h4').text('Error!');
1691 $('#return-review-alert').find('p').text(data);
1692 $('#tv-return-review').find('button[type=submit]').attr('disabled', false).find('i').remove();
1693 }
1694 },
1695 error: function(data){
1696 console.log(data);
1697 }
1698 });
1699 });
1700 */
1701
1702
1703 $('button:contains(Return)').parent('form').before(`<a class="btn btn-warning tv-return-warn" style="margin-right: 5px;">Review & Return</a>`);
1704
1705 $(document).on('click', '.tv-return-warn', function(){
1706 let currentPausedTime = pausedElapsedTime;
1707 if (paused){
1708 let now = moment.tz('America/Los_Angeles');
1709 currentPausedTime += moment(now).diff(moment(pauseDT));
1710 }
1711 let now = moment.tz('America/Los_Angeles');
1712 let interval = moment(now).diff(moment(then));
1713 let currentHourlyWage;
1714 let tempcolor = '';
1715 if (reward != 0.00 && reward != '0.00'){
1716 currentHourlyWage = (3600/((interval - currentPausedTime)/1000))*reward;
1717 $('#tvHourlyValue').text(`$${currentHourlyWage.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}/hr`);
1718 if (currentHourlyWage >= 12.50) tempcolor = 'text-success';
1719 else if (currentHourlyWage >= 7.25) tempcolor = 'text-warning';
1720 else tempcolor = 'text-danger';
1721 }
1722 let total_sec = (interval - currentPausedTime)/1000;
1723 let min = Math.floor(total_sec/60);
1724 let sec = (total_sec%60).toFixed(0);
1725 $('#return-wage-est').html(`Current Work Time: ${min < 10 ? '0' : ''}${min}min, ${sec < 10 ? '0' : ''}${sec}sec @ <span class="${tempcolor}">$${currentHourlyWage.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}/hr</span>`);
1726
1727 $('input[name=elapsed_work_time]').val((interval - currentPausedTime));
1728 $('body').addClass('global-modal-open modal-open');
1729 $('#tvReturnModal, #tv-return-modal-backdrop').show();
1730 });
1731
1732 location.assign(`javascript:$('#hourlyPause').tooltip();void(0)`);
1733 getHitReturnData(hit_set_id, assignableHitsCount);
1734
1735 getDetailedHitData(rid, reward, title, assignableHitsCount, hit_set_id);
1736
1737}
1738
1739function getDetailedHitData(requester_id, reward, title, assignableHitsCount, hit_set_id){
1740 const postData = {
1741 requester_id: requester_id,
1742 reward: reward,
1743 title: title
1744 };
1745
1746 /* Check for cached data first */
1747 let found_in_storage = false;
1748
1749 console.log('get detailed data');
1750 Object.keys(localStorage)
1751 .forEach(function(key){
1752 if (/^tvjs-hit-review/.test(key)) {
1753 let json = JSON.parse(localStorage.getItem(key));
1754 let now = moment.tz('America/Los_Angeles');
1755 let difference_ms = moment(now).diff(moment(json['date']));
1756 if (difference_ms > 450000){
1757 localStorage.removeItem(key); // delete after 7 minutes
1758 return;
1759 }
1760 if (json['hit_set_id'] != hit_set_id) return;
1761
1762 found_in_storage = true;
1763
1764 buildAndAppend(json);
1765 }
1766 });
1767
1768 if (found_in_storage) return;
1769
1770 fetch('https://view.turkerview.com/v1/hits/hit/', {
1771 method: 'POST',
1772 headers: ViewHeaders,
1773 body: JSON.stringify(postData)
1774 }).then(response => {
1775 if (!response.ok) throw response;
1776
1777 console.log(response);
1778 return response.json();
1779 }).then(json => {
1780 console.log(json);
1781 if (assignableHitsCount > 10){
1782 json['date'] = moment.tz('America/Los_Angeles');
1783 json['hit_set_id'] = hit_set_id;
1784 localStorage.setItem('tvjs-hit-review-'+hit_set_id, JSON.stringify(json));
1785 }
1786 $('body').append(buildAndAppend(json));
1787 }).catch(ex => {
1788 console.log(ex);
1789 apiExceptionHandler(ex);
1790 });
1791
1792
1793}
1794
1795function returnHitDetailDataModal(json){
1796 return `
1797<div id="taskReviewDataModal" class="modal fade in" tabindex="-1" role="dialog" style="margin-top: 2rem; display: none;">
1798 <div id="taskReviewDataDialog" class="modal-dialog" role="document">
1799 <div class="modal-content">
1800 <div class="modal-header">
1801 <button id="taskReviewDataCloseButton" type="button" class="close" data-dismiss="modal" aria-label="Close">
1802 <svg class="close-icon" viewBox="0 0 9.9 10.1">
1803 <g fill="none" stroke="#555" class="close-icon-graphic" stroke-width="2.117" stroke-linecap="round">
1804 <path d="M1.2 1.3l7.4 7.5M1.2 8.8l7.4-7.5"></path>
1805 </g>
1806 </svg>
1807 </button>
1808 <h2 class="modal-title">TurkerView HIT Details (${json.total_reviews})</h2>
1809 </div>
1810 <div class="modal-body project-details no-footer">
1811 <div class="alert alert-warning" style="font-size: 0.857rem; display: ${settings.tv_api_key.length == 40 ? `none` : `block`}">
1812 <h3>You need a valid TurkerView Auth Key</h3>
1813 <p>Please <a href="https://turkerview.com/account/api" target="_blank">register & claim your <strong>free</strong> API Key</a> by Feb 7th.</p>
1814 </div>
1815 <div class="row" style="text-align: center;">
1816 <div class="col-xs-12">
1817 <h3>Overall Average Hourly</h3>
1818 <h2 class="text-${json.avg_class}">$${(Number(json.avg_hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</h2>
1819 <h3>${moment.utc(json.avg_comp*1000).format('HH:mm:ss')}</h3>
1820 </div>
1821 </div>
1822
1823 <div class="row" style="text-align: center;">
1824 <div class="col-xs-6">
1825 <h3>Minimum Hourly</h3>
1826 <span class="text-${json.min_class}"><i class="fa fa-caret-down text-danger"></i> $${(Number(json.min_hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span>
1827 </div>
1828 <div class="col-xs-6">
1829 <h3>Maximum Hourly</h3>
1830 <span class="text-${json.max_class}"><i class="fa fa-caret-up text-success"></i> $${(Number(json.max_hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span>
1831 </div>
1832 </div>
1833
1834 <div class="row" style="text-align: center;">
1835 <h3 class="text-warning">Times are grouped by worker's speed across a variety of tasks and they may not be in order of speed on <em>this</em> particular HIT.</h3>
1836 <div class="col-xs-1"></div>
1837 <div class="col-xs-2">
1838 <h3>Careful</h3>
1839 <span class="text-${json.user_splits.careful.class}">$${(Number(json.user_splits.careful.hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}<small class="text-muted">/hr</small></span>
1840 <h3>${moment.utc(json.user_splits.careful.time*1000).format('HH:mm:ss')}</h3>
1841 <h3 style="display: ${json.user_group == 1 ? `block` : `none`}"><i class="fa fa-arrow-up"></i></h3>
1842 </div>
1843 <div class="col-xs-2">
1844 <h3>Relaxed</h3>
1845 <span class="text-${json.user_splits.relaxed.class}">$${(Number(json.user_splits.relaxed.hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}<small class="text-muted">/hr</small></span>
1846 <h3>${moment.utc(json.user_splits.relaxed.time*1000).format('HH:mm:ss')}</h3>
1847 <h3 style="display: ${json.user_group == 2 ? `block` : `none`}"><i class="fa fa-arrow-up"></i></h3>
1848 </div>
1849 <div class="col-xs-2">
1850 <h3>Average</h3>
1851 <span class="text-${json.user_splits.average.class}">$${(Number(json.user_splits.average.hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}<small class="text-muted">/hr</small></span>
1852 <h3>${moment.utc(json.user_splits.average.time*1000).format('HH:mm:ss')}</h3>
1853 <h3 style="display: ${json.user_group == 3 ? `block` : `none`}"><i class="fa fa-arrow-up"></i></h3>
1854 </div>
1855 <div class="col-xs-2">
1856 <h3>Fast</h3>
1857 <span class="text-${json.user_splits.fast.class}">$${(Number(json.user_splits.fast.hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}<small class="text-muted">/hr</small></span>
1858 <h3>${moment.utc(json.user_splits.fast.time*1000).format('HH:mm:ss')}</h3>
1859 <h3 style="display: ${json.user_group == 4 ? `block` : `none`}"><i class="fa fa-arrow-up"></i></h3>
1860 </div>
1861 <div class="col-xs-2">
1862 <h3>Proficient</h3>
1863 <span class="text-${json.user_splits.proficient.class}">$${(Number(json.user_splits.proficient.hourly)).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}<small class="text-muted">/hr</small></span>
1864 <h3>${moment.utc(json.user_splits.proficient.time*1000).format('HH:mm:ss')}</h3>
1865 <h3 style="display: ${json.user_group == 5 ? `block` : `none`}"><i class="fa fa-arrow-up"></i></h3>
1866 </div>
1867 <div class="col-xs-1"></div>
1868 <div class="col-xs-12" style="display: ${json.user_group == 0 ? `block` : `none`}"><h3 class="text-warning">You don't have enough reviews to display a work group! Consider leaving reviews in order to enable grouping.</h3></div>
1869 </div>
1870 <div class="row">
1871 <div class="col-xs-12">
1872 <h2>Work Groups</h2>
1873 <ul class="text-muted">
1874 <li><h3 class="text-muted">Careful - Careful readers tend to take their time, about an average worker with less than 5,000 HITs completed</h3></li>
1875 <li><h3 class="text-muted">Relaxed - these workers most often move at a pace you'd expect for someone who has completed 5,000-10,000 HITs</h3></li>
1876 <li><h3 class="text-muted">Average - these are workers who tend to stay close to the average / middle of the pack of workers on TV.</h3></li>
1877 <li><h3 class="text-muted">Fast - Workers who have completed thousands of tasks & likely use faster computers with organized workflows</h3></li>
1878 <li><h3 class="text-muted">Proficient - These workers tend to have tens of thousands of HITs under their belt along with special scripts & keybinds to complete work faster.</h3></li>
1879 </ul>
1880 <h3 class="text-muted"><i class="fa fa-arrow-up"></i> - Your group (users you're most likely to have a completion time close to) - you'll need to leave 50+ reviews before this starts becoming reliable</h3>
1881 </div>
1882 </div>
1883
1884 <div class="row m-t-lg m-b-0" style="text-align: center;">
1885 <div class="col-xs-6"><a href="https://turkerview.com/requesters/${json.requester_id}" target="_blank">Overview</a></div>
1886 <div class="col-xs-6"><a href="https://turkerview.com/requesters/${json.requester_id}/reviews" target="_blank">Reviews</a></div>
1887 </div>
1888 </div>
1889 </div>
1890 </div>
1891</div>
1892<div id="taskReviewDataBackdrop" class="modal-backdrop fade in" style="display: none;"></div>
1893 `;
1894}
1895
1896/* Detailed HIT Record Functions */
1897
1898function showTaskReviewData(){
1899 document.getElementById(`taskReviewDataModal`).style.display = `block`;
1900 document.getElementById(`taskReviewDataBackdrop`).style.display = `block`;
1901 document.querySelector(`body`).classList.add(`global-modal-open`);
1902 document.querySelector(`body`).classList.add(`modal-open`);
1903}
1904
1905function hideTaskReviewData(){
1906 document.getElementById(`taskReviewDataModal`).style.display = `none`;
1907 document.getElementById(`taskReviewDataBackdrop`).style.display = `none`;
1908 document.querySelector(`body`).classList.remove(`global-modal-open`);
1909 document.querySelector(`body`).classList.remove(`modal-open`);
1910}
1911
1912function taskHourlyTVClass(hourly) {
1913 if (hourly == null) return `unrated`;
1914
1915 if (hourly > 10.50) return `mts-green`;
1916 if (hourly > 7.25) return `mts-orange`;
1917 if (hourly > 0.0) return `mts-red`;
1918 return `unrated`;
1919}
1920
1921function buildAndAppend(json, assignableHitsCount){
1922 if (json.avg_hourly === null) return;
1923
1924 document.querySelector(`body`).insertAdjacentHTML(`beforeend`, returnHitDetailDataModal(json));
1925
1926 let file = taskHourlyTVClass(json.avg_hourly);
1927
1928 document.querySelectorAll(`.work-pipeline-action`).forEach(el => {
1929 el.insertAdjacentHTML(`afterbegin`, `<a class="btn btn-secondary tv-task-review-data" href="#" style="margin-right: 5px;"><img src="https://turkerview.com/assets/images/tv-${file}.png" style="max-height: 14px;"></img></a>`);
1930 })
1931
1932 document.querySelectorAll(`.task-project-title`).forEach(el => {
1933 el.insertAdjacentHTML(`afterbegin`, `<div class="tv-task-review-data" style="display: inline-block; margin-right: 4px; cursor: pointer;"><img src="https://turkerview.com/assets/images/tv-${file}.png" style="max-height: 16px;"></img></div>`);
1934 })
1935
1936 document.getElementById(`taskReviewDataModal`).addEventListener(`click`, function(){
1937 hideTaskReviewData()
1938 })
1939
1940 document.getElementById(`taskReviewDataCloseButton`).addEventListener(`click`, function(){
1941 hideTaskReviewData()
1942 })
1943
1944 document.querySelectorAll(`.tv-task-review-data`).forEach(el => {
1945 el.addEventListener(`click`, function(){
1946 showTaskReviewData();
1947 })
1948 })
1949
1950 document.getElementById(`taskReviewDataDialog`).addEventListener(`click`, function(e){
1951 e.stopPropagation();
1952 });
1953}
1954
1955function getHitReturnData(group_id, assignableHitsCount){
1956 //run through localstorage.keys for stored data
1957 //if it exists for this hit, check the datetime and if its less than a few hours old go ahead & just use it
1958 //make sure to delete old data [ie, >1 day old]
1959 //if it doesn't exist, go ahead & make a call to tv to retrieve it
1960 //if >15 or so tasks, go ahead & store it in case its a batch
1961 let found_in_storage = false;
1962 Object.keys(localStorage)
1963 .forEach(function(key){
1964 if (/^tv-return-data/.test(key)) {
1965 let json = JSON.parse(localStorage.getItem(key));
1966 let now = moment.tz('America/Los_Angeles');
1967 let difference_ms = moment(now).diff(moment(json['date']));
1968 if (difference_ms > 450000){
1969 localStorage.removeItem(key);
1970 return;
1971 }
1972 if (json['group_id'] != group_id) return;
1973
1974 found_in_storage = true;
1975
1976 parseBuildReturnWarnings(json);
1977 }
1978 });
1979
1980 if (found_in_storage) return;
1981
1982 let exp = settings.exp_returners ? '&exp=true' : '';
1983 fetch(`https://view.turkerview.com/v1/returns/retrieve/?hit_set_id=${group_id}${exp}`, {
1984 method: 'GET',
1985 cache: 'no-cache',
1986 headers: ViewHeaders
1987 }).then(response => {
1988 if (!response.ok) throw response;
1989
1990 return response.json();
1991 }).then(data => {
1992 let reward = 0;
1993 let total_time_in_ms = 0;
1994 let underpaid_total = 0;
1995 let broken_total = 0;
1996 let unpaid_screener_total = 0;
1997 let screened_time_total = 0;
1998 let tos_total = 0;
1999 let writing_total = 0;
2000 let downloads_total = 0;
2001 let em_total = 0;
2002 let comments = [];
2003 for (i = 0; i < data.length; i++){
2004 let localUserIgnore = JSON.parse(localStorage.getItem('tv-return-ignore-list')) || [];
2005 if (localUserIgnore.includes(data[i].user_id)) continue;
2006
2007
2008 if (data[i].unpaid_screener == 1 && (data[i].elapsed_work_time/1000) < settings.rr_screener_min) continue;
2009
2010 reward = data[i].reward;
2011 total_time_in_ms += data[i].elapsed_work_time;
2012 underpaid_total += data[i].underpaid;
2013 broken_total += data[i].broken;
2014 unpaid_screener_total += data[i].unpaid_screener;
2015 tos_total += data[i].tos;
2016 writing_total += data[i].writing;
2017 downloads_total += data[i].downloads;
2018 em_total += data[i].extraordinary_measures;
2019
2020 let comment_prefix = '';
2021 if (data[i].underpaid == 1){
2022 //user said this was underpaid, throw their hourly @ time of return at the beginning of their comment
2023
2024 let hourly = (3600/(data[i].elapsed_work_time/1000))*(parseFloat(reward)).toFixed(2);
2025 comment_prefix = `[Marked Underpaid @ $${hourly.toFixed(2)}/hr]<br>`;
2026 }
2027
2028 if (data[i].unpaid_screener == 1){
2029 //user said this was underpaid, throw their hourly @ time of return at the beginning of their comment
2030 screened_time_total += data[i].elapsed_work_time;
2031
2032 let time_in_seconds = data[i].elapsed_work_time/1000;
2033 let min = Math.floor(time_in_seconds/60);
2034 let sec = (time_in_seconds%60).toFixed(0);
2035 comment_prefix += `[Screened out @${min < 10 ? '0' : ''}${min}:${sec < 10 ? '0' : ''}${sec}]<br>`;
2036 }
2037
2038 if (data[i].tos == 1) comment_prefix += `${tosMap.get(data[i].tos_type)}<br>`;
2039
2040 if (data[i].writing == 1) comment_prefix += `${writingMap.get(data[i].writing_type)}<br>`;
2041 if (data[i].downloads == 1) comment_prefix += `${downloadsMap.get(data[i].downloads_type)}<br>`;
2042 if (data[i].extraordinary_measures == 1) comment_prefix += `${emMap.get(data[i].em_type)}<br>`;
2043
2044 let comment_object = {
2045 user_id: data[i].user_id,
2046 username: data[i].username,
2047 date: data[i].date,
2048 tos_type: data[i].tos_type,
2049 comment: comment_prefix + data[i].comment }
2050 comments.push(comment_object);
2051 }
2052
2053 let objectToStore = {
2054 group_id: group_id,
2055 date: moment.tz('America/Los_Angeles'),
2056 reward: reward,
2057 total_time_in_ms: total_time_in_ms,
2058 underpaid_total: underpaid_total,
2059 broken_total: broken_total,
2060 unpaid_screener_total: unpaid_screener_total,
2061 unpaid_screener_time: screened_time_total,
2062 tos_total: tos_total,
2063 writing_total: writing_total,
2064 downloads_total: downloads_total,
2065 em_total: em_total,
2066 comments: comments
2067 };
2068
2069 parseBuildReturnWarnings(objectToStore);
2070
2071 if (assignableHitsCount >= 10){
2072 //lets store this in case its a batch.
2073 localStorage.setItem('tv-return-data-'+group_id, JSON.stringify(objectToStore));
2074 }
2075 }).catch(ex => {
2076 apiExceptionHandler(ex);
2077 });
2078
2079}
2080
2081function parseBuildReturnWarnings(json){
2082 if (json['broken_total'] + json['underpaid_total'] + json['unpaid_screener_total'] + json['tos_total'] + json['writing_total'] + json['downloads_total'] == 0) return;
2083
2084 let x = Number(json['broken_total']) + Number(json['underpaid_total']) + Number(json['unpaid_screener_total']) + Number(json['tos_total']) + Number(json['writing_total']) + Number(json['downloads_total']);
2085
2086 $('.tv-return-warning-data-launcher').append()
2087 let high_alert = [];
2088 let medium_alert = [];
2089 let low_alert = [];
2090 for(var warning_level in settings.return_warning_levels){
2091 if (settings.return_warning_levels.hasOwnProperty(warning_level)){
2092 if (settings.return_warning_levels[warning_level] == 'high') high_alert.push(warning_level);
2093 else if (settings.return_warning_levels[warning_level] == 'medium') medium_alert.push(warning_level);
2094 else if (settings.return_warning_levels[warning_level] == 'low') low_alert.push(warning_level);
2095 }
2096 }
2097
2098 //oh, this is going to be ugly, whoops, TODO: clean this up stop coding like a monkey
2099 let highest_warning_class = 'hidden';
2100
2101 //low
2102 if (json["underpaid_total"] > 0 && low_alert.indexOf("underpaid") > -1) highest_warning_class = 'text-muted';
2103 else if (json["broken_total"] > 0 && low_alert.indexOf("broken") > -1) highest_warning_class = 'text-muted';
2104 else if (json["unpaid_screener_total"] > 0 && low_alert.indexOf("screener") > -1) highest_warning_class = 'text-muted';
2105 else if (json["tos_total"] > 0 && low_alert.indexOf("tos") > -1) highest_warning_class = 'text-muted';
2106 else if (json["writing_total"] > 0 && low_alert.indexOf("writing") > -1) highest_warning_class = 'text-muted';
2107 else if (json["downloads_total"] > 0 && low_alert.indexOf("downloads") > -1) highest_warning_class = 'text-muted';
2108 else if (json["em_total"] > 0 && low_alert.indexOf("extraordinary_measures") > -1) highest_warning_class = 'text-muted';
2109
2110 //med
2111 if (json["underpaid_total"] > 0 && medium_alert.indexOf("underpaid") > -1) highest_warning_class = 'text-warning';
2112 else if (json["broken_total"] > 0 && medium_alert.indexOf("broken") > -1) highest_warning_class = 'text-warning';
2113 else if (json["unpaid_screener_total"] > 0 && medium_alert.indexOf("screener") > -1) highest_warning_class = 'text-warning';
2114 else if (json["tos_total"] > 0 && medium_alert.indexOf("tos") > -1) highest_warning_class = 'text-warning';
2115 else if (json["writing_total"] > 0 && medium_alert.indexOf("writing") > -1) highest_warning_class = 'text-warning';
2116 else if (json["downloads_total"] > 0 && medium_alert.indexOf("downloads") > -1) highest_warning_class = 'text-warning';
2117 else if (json["em_total"] > 0 && medium_alert.indexOf("extraordinary_measures") > -1) highest_warning_class = 'text-warning';
2118
2119 //high
2120 if (json["underpaid_total"] > 0 && high_alert.indexOf("underpaid") > -1) highest_warning_class = 'text-danger';
2121 else if (json["broken_total"] > 0 && high_alert.indexOf("broken") > -1) highest_warning_class = 'text-danger';
2122 else if (json["unpaid_screener_total"] > 0 && high_alert.indexOf("screener") > -1) highest_warning_class = 'text-danger';
2123 else if (json["tos_total"] > 0 && high_alert.indexOf("tos") > -1) highest_warning_class = 'text-danger';
2124 else if (json["writing_total"] > 0 && high_alert.indexOf("writing") > -1) highest_warning_class = 'text-danger';
2125 else if (json["downloads_total"] > 0 && high_alert.indexOf("downloads") > -1) highest_warning_class = 'text-danger';
2126 else if (json["em_total"] > 0 && high_alert.indexOf("extraordinary_measures") > -1) highest_warning_class = 'text-danger';
2127
2128
2129 if (settings.show_return_favicon){
2130 let href = 'https://worker.mturk.com/favicon.ico';
2131 if (highest_warning_class == 'text-muted') href = 'null';
2132 else if (highest_warning_class == 'text-warning') href = 'null';
2133 else if (highest_warning_class == 'text-danger') href = 'null';
2134 var favicon = document.querySelector("link[rel*='icon']") || document.createElement('link');
2135 favicon.type = 'image/x-icon';
2136 favicon.rel = 'shortcut icon';
2137 favicon.href = href;
2138 //document.getElementsByTagName('head')[0].appendChild(favicon);
2139 }
2140
2141
2142 $('.work-pipeline-action').prepend(`<a ${highest_warning_class == 'hidden' ? 'style="display: none;"' : ''} class="btn btn-danger tv-return-warning-data-launcher" href="#" style="margin-right: 5px;"><i class="fa fa-fw fa-warning"></i> ${x}</a>`);
2143 //$('.task-project-title').before(`<div ${highest_warning_class == 'hidden' ? 'style="display: none;"' : ''}><i class="fa fa-warning ${highest_warning_class} tv-return-warning-data-launcher" style="cursor: pointer; padding-right: 3px;"></i></div>`);
2144
2145 document.querySelectorAll(`.task-project-title`).forEach(el => {
2146 el.insertAdjacentHTML(`afterbegin`, `<div ${highest_warning_class == 'hidden' ? 'style="display: none;"' : 'style="display: inline-block;"'}><i class="fa fa-warning ${highest_warning_class} tv-return-warning-data-launcher" style="cursor: pointer; padding-right: 3px;"> (${x})</i></div>`);
2147 })
2148
2149 $('#hourlyContainer').append(`<i class="fa fa-warning fa-fw ${highest_warning_class} pull-right tv-return-warning-data-launcher" style="line-height: 1rem; cursor: pointer; ${highest_warning_class == 'hidden' ? 'display: none;' : ''}" data-toggle="tooltip" data-title="Oh no!"></i>`);
2150
2151 let brokenClass = classMap(json['broken_total']);
2152 let underpaidClass = classMap(json['underpaid_total']);
2153 let screenerClass = classMap(json['unpaid_screener_total']);
2154 let tosClass = classMap(json['tos_total']);
2155 let writingClass = classMap(json['writing_total']);
2156 let downloadsClass = classMap(json['downloads_total'])
2157 let emClass = classMap(json['em_total']);
2158
2159 let unpaid_screening_html = `<span class="text-muted">No users have reported being screened out without payment for this HIT.</span>`;
2160 if (json['unpaid_screener_total'] > 0){
2161 let average_ms_screened = json['unpaid_screener_time']/json['unpaid_screener_total'];
2162 let time_in_seconds = average_ms_screened/1000;
2163 let min_screen = Math.floor(time_in_seconds/60);
2164 let sec_screen = (time_in_seconds%60).toFixed(0);
2165 unpaid_screening_html = `<span class="text-warning">${json['unpaid_screener_total'] == 1 ? `1 user has` : `${json['unpaid_screener_total']} users have`} spent an average of <span class="text-danger">${min_screen}m:${sec_screen}s</span> on the screening questions.</span>`;
2166 }
2167
2168 let commentHTML = '';
2169 if (json['comments'].length == 0) commentHTML = `<span class="text-muted">No user comments exist for this HIT</span>`;
2170
2171 let no_comment_userlist = `<small class="text-muted">Users who did not comment: `;
2172 for(i = 0; i < json['comments'].length; i++){
2173 if (json['comments'][i]['comment'] == "") {
2174 no_comment_userlist += `<a href="https://turkerview.com/users/?user=${json['comments'][i]['user_id']}" target="_blank">${json['comments'][i]['username']}</a> [ <a class="text-danger" style="cursor: pointer;" onclick="ignoreUser(${json['comments'][i]['user_id']}, '${json['comments'][i]['username']}')"><i class="fa fa-ban"></i></a> ], `;
2175 continue;
2176 }
2177 commentHTML += `
2178 <p style="margin-bottom: 0;">${moment(json['comments'][i]['date'], 'X').fromNow()} <a href="https://turkerview.com/users/?user=${json['comments'][i]['user_id']}" target="_blank">${json['comments'][i]['username']}</a> [ <a class="text-danger" style="cursor: pointer;" onclick="ignoreUser(${json['comments'][i]['user_id']}, '${json['comments'][i]['username']}')"><i class="fa fa-ban"></i></a> ]</small> said: </p>
2179 <blockquote class="blockquote" style="font-size: .85rem; margin-left: 15px;">${json['comments'][i]['comment']}</blockquote>`;
2180 }
2181 no_comment_userlist = no_comment_userlist.slice(0, -2);
2182 no_comment_userlist += `</small>`;
2183
2184 let tvReturnWarningDataModal = `
2185<div class="modal fade in" id="tvReturnWarningDataModal" style="display: none;">
2186 <div id="tv-return-warning-data-dialog" class="modal-dialog">
2187 <div class="modal-content">
2188 <div class="modal-header">
2189 <button id="tv-return-warning-data-modal-close" type="button" class="close" data-dismiss="modal"><svg class="close-icon" data-reactid=".8.0.0.0.0.0"><g fill="none" stroke="#555" class="close-icon-graphic" stroke-width="2.117" stroke-linecap="round" data-reactid=".8.0.0.0.0.0.0"><path d="M1.2 1.3l7.4 7.5M1.2 8.8l7.4-7.5" data-reactid=".8.0.0.0.0.0.0.0"></path></g></svg></button>
2190 <h2 class="modal-title">TurkerView Return Warning Report</h2>
2191 </div>
2192 <div class="modal-body">
2193 <div class="alert alert-warning" style="${($('iframe').attr('src').includes('/evaluation/endor')) ? '' :'display: none;'}">
2194 <h4>We're on Endor</h4>
2195 <p>This HIT is from a Google Requester (Endor) - they often limit the # of HITs workers can complete which is confusing & gets reported as broken. Generally, they are "safe" to work for so consider checking their full TV profile!</p>
2196 </div>
2197 <div id="return-review-warning" class="alert alert-dismissable alert-warning" role="alert" style="display: none;">
2198 <button type="button" id="close-return-review-warning" class="close" data-dismiss="alert">×</button>
2199 <h4>Heads Up!</h4>
2200 <p>While we make every effort to ensure data on TurkerView is accurate & high-quality this feature is new & highly experimental! Make sure to preview comments/user profiles to make sure data is accurate & make a more informed decision :)</p>
2201 </div>
2202 <div id="return-ignore-user-failure" class="alert alert-dismissable alert-danger hide">
2203 <button type="button" class="close" data-dismiss="alert">×</button>
2204 <h4>No Bueno!</h4>
2205 <p>You couldn't ignore the user!</p>
2206 </div>
2207 <div class="row">
2208 <h3>Reasons for returning</h3>
2209 <div class="row">
2210 <div class="col-xs-12 text-muted">
2211 <div class="col-xs-3" style="text-align: center;">
2212 <h1 class="${brokenClass}" style="margin-bottom: 0;">${json['broken_total']}</h1>
2213 <small class="text-muted">Broken Returns</small>
2214 </div>
2215 <div class="col-xs-3" style="text-align: center;">
2216 <h1 class="${underpaidClass}" style="margin-bottom: 0;">${json['underpaid_total']}</h1>
2217 <small class="text-muted">Underpaid Returns</small>
2218 </div>
2219 <div class="col-xs-3" style="text-align: center;">
2220 <h1 class="${screenerClass}" style="margin-bottom: 0;">${json['unpaid_screener_total']}</h1>
2221 <small class="text-muted">Unpaid Screener Reports</small>
2222 </div>
2223 <div class="col-xs-3" style="text-align: center;">
2224 <h1 class="${tosClass}" style="margin-bottom: 0;">${json['tos_total']}</h1>
2225 <small class="text-muted">TOS Violations</small>
2226 </div>
2227 </div>
2228 </div>
2229 <div>
2230 <div class="col-xs-12 text-muted">
2231 <div class="col-xs-4" style="text-align: center;">
2232 <h1 class="${writingClass}" style="margin-bottom: 0;">${json['writing_total']}</h1>
2233 <small class="text-muted">Writing Returns</small>
2234 </div>
2235 <div class="col-xs-4" style="text-align: center;">
2236 <h1 class="${downloadsClass}" style="margin-bottom: 0;">${json['downloads_total']}</h1>
2237 <small class="text-muted">Downloads Warnings</small>
2238 </div>
2239 <div class="col-xs-4" style="text-align: center;">
2240 <h1 class="${emClass}" style="margin-bottom: 0;">${json['em_total']}</h1>
2241 <small class="text-muted">Extraordinary Measures Warnings</small>
2242 </div>
2243 </div>
2244 </div>
2245 </div>
2246 <div class="row">
2247 <h3>Unpaid Screening Data</h3>
2248 <div class="col-xs-12 text-muted">
2249 ${unpaid_screening_html}
2250 </div>
2251 </div>
2252 <div class="row" style="margin-bottom: 0">
2253 <h3>User Comments</h3>
2254 <div class="col-xs-12 text-muted">
2255 ${commentHTML}
2256 ${no_comment_userlist}
2257 </div>
2258 </div>
2259 </div>
2260 </div>
2261 </div>
2262</div>
2263<script>
2264function ignoreUser(user_id, username){
2265 if (!confirm("Are you sure you want to ignore " + username + "? All of their return reviews will be hidden from you & not displayed at all.")) return;
2266
2267 fetch('https://turkerview.com/api/v1/users/ignore/?user_id='+user_id, {
2268 method: 'GET',
2269 headers: {
2270 'Access-Control-Allow-Headers': 'Access-Control-Allow-Headers'
2271 },
2272 credentials: 'include'
2273 }).then(res => {
2274 if (!res.ok) throw res;
2275 return res.json();
2276 }).then(res => {
2277 let localUserIgnore = JSON.parse(localStorage.getItem('tv-return-ignore-list')) || [];
2278 if (!localUserIgnore.includes(user_id)) localUserIgnore.push(user_id);
2279 localStorage.setItem('tv-return-ignore-list', JSON.stringify(localUserIgnore));
2280 $('#return-review-warning').show().find('p').html('You have ignored '+username+'. You can undo this operation in your <a href="https://turkerview.com/users/preferences/" target="_blank" style="text-decoration: underline;">user preferences</a>.');
2281 })
2282 .catch(responseError => {
2283 if (responseError.status == 401) $('#return-ignore-user-failure').show().find('p').html('You could not ignore this user as you are not logged in to <a href="https://turkerview.com" target="_blank" style="text-decoration: underline;">TurkerView</a>. If you visit the site & are already logged in you need to enabled third party cookies: <a href="https://forum.turkerview.com/posts/952316/" target="_blank" style="text-decoration: underline;">Chrome</a> | <a href="https://forum.turkerview.com/posts/967296/" target="_blank" style="text-decoration: underline;">FireFox</a>');
2284 if (responseError.status == 422) $('#return-ignore-user-failure').show().find('p').html('Oh no, if we have to put up with you so do you. You may not ignore yourself.');
2285 if (responseError.status == 409) {
2286 let localUserIgnore = JSON.parse(localStorage.getItem('tv-return-ignore-list')) || [];
2287 if (!localUserIgnore.includes(user_id)) localUserIgnore.push(user_id);
2288 localStorage.setItem('tv-return-ignore-list', JSON.stringify(localUserIgnore));
2289 $('#return-ignore-user-failure').show().find('p').html('Looks like you have already ignored '+username+'. We re-added them to your local cache.');
2290 }
2291 });
2292 }
2293</script>
2294<div id="tv-return-warning-data-modal-backdrop" class="modal-backdrop fade in" style="display: none;"></div>`;
2295
2296 $('footer').after(tvReturnWarningDataModal);
2297
2298 $('.close').click(function(){
2299 $(this).parent().hide();
2300 });
2301
2302 $('#tv-return-warning-data-modal-backdrop, #tv-return-warning-data-modal-close, #tvReturnWarningDataModal').on('click', function(){
2303 $('#tv-return-warning-data-modal-backdrop, #tvReturnWarningDataModal').hide();
2304 $('body').removeClass('global-modal-open modal-open');
2305 });
2306
2307 $('#tv-return-warning-data-dialog').click(function(e){
2308 e.stopPropagation();
2309 });
2310
2311 $('.tv-return-warning-data-launcher').click(function(){
2312 $('body').addClass('global-modal-open modal-open');
2313 $('#tvReturnWarningDataModal, #tv-return-warning-data-modal-backdrop').show();
2314 });
2315
2316
2317}
2318
2319function contactPage(){
2320 if (window.location.href.includes('TURKERVIEW')){
2321 //lets give folks a warning they're going to see an error
2322 $('strong:contains(Subject)').prepend(`
2323<div id="contact-error-warning" class="alert alert-warning">
2324 <h4>Heads Up from TurkerView!</h4>
2325 <p>After you send your message MTurk will give you a warning on the next page saying "something is not right" - it's a glitch in their routing from manual contact links but <u>your message to the requester goes through just fine</u>!</p>
2326</div>`);
2327 }
2328
2329 let contact_requester_id = $('#hit_type_message_requester_id').length > 0 ? $('#hit_type_message_requester_id').val() : null;
2330
2331 if (contact_requester_id == null) {
2332 contact_requester_id = window.location.href.match(/requester_id=([A-Z0-9]+)/)[1];
2333 }
2334
2335 Object.keys(localStorage)
2336 .forEach(function(key){
2337 if (/^tv_/.test(key)) {
2338 let json = JSON.parse(localStorage.getItem(key));
2339
2340 if (json.rid != contact_requester_id) return;
2341 json.contacted = true;
2342 localStorage.setItem(key, JSON.stringify(json));
2343 }
2344 });
2345}
2346
2347let runOnce = 0;
2348function getRIDs(){
2349
2350 if (runOnce > 0) return;
2351
2352 runOnce++;
2353 $('.desktop-row').each(function(){
2354 let thisRow = $(this);
2355 let requester_id = thisRow.find('a[href*="/requesters/"]').attr('href').match(/requesters\/(.*?)\//)[1]
2356 if (settings.display_requester_ratings != false) thisRow.find('a[href*="/requesters/"]').before(`<div class="tv-container" style="display: inline-block;"><span class="turkerview" title="" style="cursor: pointer; height: 18px; width: 18px; background-image: url('https://turkerview.com/assets/images/tv-unrated.png'); background-size: cover; background-position-y: 3px; background-repeat: no-repeat; opacity: 0.25;"></span></div>`);
2357 if (settings.display_hit_ratings != false) thisRow.find('.project-name-column').prepend(`<div class="tv-container" style="display: inline-block;"><span class="turkerview" title="" style="cursor: pointer; height: 18px; width: 18px; background-image: url('https://turkerview.com/assets/images/tv-unrated.png'); background-size: cover; background-position-y: 3px; background-repeat: no-repeat; opacity: 0.25;"></span></div>`);
2358
2359
2360 var popoverHTML = buildDefaultPopover(requester_id);
2361 thisRow.children('.requester-column').find('.tv-container').append(popoverHTML);
2362 })
2363 let rids = [];
2364 let hitKeys = [];
2365
2366 //return; //testing what happens if no response from tv (might happen first few days of new setup)
2367 react = $('ol').parent('div').data('react-props').bodyData;
2368
2369 $.each(react, function(i, v) {
2370 //quick fix for queue since data is nested 2 deep in the array.
2371 if (queue.test(windowHREF)){
2372 if ($.inArray(v.project.requester_id, rids) === -1) rids.push(v.project.requester_id);
2373 hitKeys.push(v.project.requester_id.concat(v.project.monetary_reward.amount_in_dollars.toFixed(2), v.project.title));
2374 } else {
2375 if ($.inArray(v.requester_id, rids) === -1) rids.push(v.requester_id);
2376 hitKeys.push(v.requester_id.concat(v.monetary_reward.amount_in_dollars, v.title));
2377 }
2378
2379 });
2380
2381 if (rids.length === 0) return;
2382
2383 //fetch requester data
2384 if (settings.display_requester_ratings != false){
2385 fetch(`https://view.turkerview.com/v1/requesters/?requester_ids=${rids.join(',')}`, {
2386 method: 'GET',
2387 cache: 'no-cache',
2388 headers: ViewHeaders
2389 }).then(response => {
2390 if (!response.ok) throw response;
2391
2392 return response.json();
2393 }).then(json => {
2394 viewData = json;
2395 buttonUp(viewData.requesters, react);
2396 }).catch(ex => {
2397 apiExceptionHandler(ex);
2398 });
2399 }
2400
2401
2402 //fetch hit data
2403 if (settings.display_hit_ratings != false){
2404 fetch('https://view.turkerview.com/v1/hits/', {
2405 method: 'POST',
2406 headers: ViewHeaders,
2407 body: JSON.stringify(hitKeys)
2408 }).then(response => {
2409 if (!response.ok) throw response;
2410
2411 return response.json();
2412 }).then(json => {
2413 hitData = json;
2414 buttonHitUp(hitData, react)
2415 }).catch(ex => {
2416 console.log(ex);
2417 apiExceptionHandler(ex);
2418 });
2419 }
2420
2421
2422}
2423
2424function apiExceptionHandler(exception){
2425 /*
2426 Current exceptions that should be handled [code: message]
2427 401: invalidUserAuthKey - no or incorrect user api key provided, notify user to register and/or check their API key on TurkerView (free for ~2-3hrs of usage/day)
2428 401: invalidApplicationKey - no or incorrect application identifier provided, please register your application w/ TurkerView (its free)
2429 403: dailyLimitExceeded - user has run out of free API calls, stop sending requests & notify user to upgrade or wait until tomorrow
2430
2431 Exception text can be accessed with ex.statusText
2432 */
2433
2434 if ($('#tvjs-view-error').length > 0) return;
2435 if (exception.statusText == 'invalidUserAuthKey') $('#MainContent').prepend(`
2436<div id="tvjs-view-error" class="alert alert-danger">
2437 <h4>Your TurkerView API Key is invalid.</h4>
2438 <p>You can claim your free API key (or support the site with a subscription!) from your <a href="https://turkerview.com/account/api/" target="_blank" style="text-decoration: underline;">TurkerView account API dashboard.</a></p>
2439</div>`);
2440 else if (exception.statusText == 'dailyLimitExceeded') $('#MainContent').prepend(`
2441<div id="tvjs-view-error" class="alert alert-danger">
2442 <h4>Your TurkerView API Key has hit its free limit.</h4>
2443 <p>Please upgrade to a subscription plan from your <a href="https://turkerview.com/account/api/" target="_blank" style="text-decoration: underline;">TurkerView account API dashboard</a>.</p>
2444</div>`);
2445}
2446
2447function buttonUp(viewData, react){
2448 //lets lock ourselves into the desktop mode and see if anyone complains about TVJS not working on mobile
2449 let noData = `<i class="fa fa-minus" style="color: rgba(128, 128, 128, 1);"></i> No Data`;
2450 $('.table-row-popover__content').css('display', 'none');
2451
2452 console.log(react);
2453 $('.desktop-row').each(function(rowNum){
2454 let thisRow = $(this);
2455 let rid = !react[rowNum].project ? react[rowNum].requester_id : react[rowNum].project.requester_id;
2456 let row_reward = !react[rowNum].project ? react[rowNum].monetary_reward.amount_in_dollars : react[rowNum].project.monetary_reward.amount_in_dollars;
2457 let user_html = ``;
2458
2459 if (viewData[rid] && viewData[rid].wages.user_average.wage !== null){
2460 user_html = `<div style="display: flex;">
2461 <div style="flex: 1;">
2462 <span class="tv-td">Pay Rate:</span>
2463 </div>
2464 <div style="flex: 1;">
2465 <span class="tv-td"><span class="text-${viewData[rid].wages.user_average.class}">$</i>${viewData[rid].wages.user_average.wage}<span style="font-size: 85%;"> / hr</span></span></span>
2466 </div>
2467 </div>`;
2468 } else {
2469 user_html = `<p class="text-muted" style="text-align: center;">You haven't reviewed this requester!</p><p style="text-align: center; margin-bottom: 0;"><a href="http://turkerview.com/review.php" target="_blank">Learn How!</a></p> `
2470 }
2471
2472
2473 let params = {
2474 reviewCount: viewData[rid] ? viewData[rid].reviews : 0,
2475 userReviewCount: viewData[rid] ? viewData[rid].user_reviews : 0,
2476 hourly: (viewData[rid] && viewData[rid].wages.average.wage !== null) ? `<span class="text-${viewData[rid].wages.average.class}">$</i>${viewData[rid].wages.average.wage}<span style="font-size: 85%;"> / hr</span></span>` : noData,
2477 pay: viewData[rid] ? `<span class="text-${viewData[rid].ratings.pay.class}">${viewData[rid].ratings.pay.text} <i class="fa ${viewData[rid].ratings.pay.faicon}"></i></span>` : noData,
2478 approval: viewData[rid] ? `<span class="text-${viewData[rid].ratings.fast.class}">${viewData[rid].ratings.fast.text}</span>` : noData,
2479 comm: viewData[rid] ? `<span class="text-${viewData[rid].ratings.comm.class}">${viewData[rid].ratings.comm.text}</span>` : noData,
2480 rname: !react[rowNum].project ? react[rowNum].requester_name : react[rowNum].project.requester_name,
2481 rid: rid,
2482 title: !react[rowNum].project ? react[rowNum].title : react[rowNum].project.title,
2483 reward: !react[rowNum].project ? react[rowNum].monetary_reward.amount_in_dollars : react[rowNum].project.monetary_reward.amount_in_dollars,
2484 rejections: !viewData[rid] ? '<i class="fa fa-minus" style="color: rgba(128, 128, 128, 1);"></i> No Data' :
2485 viewData[rid].rejections === 0 ? '<i class="fa fa-check" style="color: rgba(0, 128, 0, 1);"></i> No Rejections' : '<i class="fa fa-times" style="color: rgba(255, 0, 0, 1);"></i> Rejected Work',
2486 blocks: !viewData[rid] ? '<i class="fa fa-minus" style="color: rgba(128, 128, 128, 1);"></i> No Data' :
2487 viewData[rid].blocks === 0 ? '<i class="fa fa-check" style="color: rgba(0, 128, 0, 1);"></i> No Blocks' : '<i class="fa fa-times" style="color: rgba(255, 0, 0, 1);"></i> Blocks Reported',
2488 user_data: {
2489 html_template: user_html,
2490 hourly: (viewData[rid] && viewData[rid].wages.user_average.wage !== null) ? `<span class="text-${viewData[rid].wages.user_average.class}">$</i>${viewData[rid].wages.user_average.wage}<span style="font-size: 85%;"> / hr</span></span>` : noData
2491 },
2492 requester_info: queue.test(windowHREF) ? null : react[rowNum].requesterInfo
2493 };
2494 let opacity = viewData[rid] ? confidence(viewData[rid].reviews) : '0.5';
2495
2496 let hourlyAvgForIcon = (viewData[rid] && viewData[rid].wages.average.wage) ? viewData[rid].wages.average.wage : null;
2497 thisRow.children('.requester-column').find('.turkerview').css('background-image', `url(${iconImage(hourlyAvgForIcon)})`).css('opacity', `1`);
2498 var popoverHTML = buildVIEWPopover(params);
2499 thisRow.children('.requester-column').find('.tv-container').append(popoverHTML);
2500 thisRow.children('.requester-column').find('.tv-popover-placeholder').remove();
2501 if (!queue.test(windowHREF)) thisRow.children('.requester-column').find('.tv-container').before(`<i class="fa text-muted fa-amazon mturk-requester-info" style="${settings.display_mturk_ratings == false ? 'display: none;' : ``}"></i>`);
2502
2503
2504 //rejections? blocks?
2505 if (params.rejections.indexOf('Rejected Work') >= 0) {
2506 thisRow.find('.requester-column').find('.tv-container:last').after(`<i style="cursor: pointer; font-size: inherit; color: rgba(255, 0, 0, 0.25); margin: 0;" class="fa fa-exclamation-circle tv-rejection-btn turkerview tv-tooltip" data-toggle="tooltip" data-title="Rejections Reported! Click for more info." data-requester_id="${rid}" data-reward="${row_reward}"></i>`);
2507 }
2508 if (params.blocks.indexOf('Blocks Reported') >= 0) thisRow.find('.requester-column').find('.tv-container:last').after(`<i style="font-size: inherit; color: rgba(255, 0, 0, 0.25); margin: 0" class="fa fa-exclamation-triangle turkerview tv-tooltip" data-toggle="tooltip" data-title="Account Blocks Reported!"></i>`);
2509 }); //end row loop
2510
2511 //ugly hack to get around sandboxing
2512 location.assign("javascript:$('.tv-tooltip').tooltip();void(0)");
2513
2514 $('.tv-rejection-btn').click(function(e) {
2515 e.stopPropagation();
2516 let btn = $(this);
2517
2518 $('body').addClass('global-modal-open modal-open');
2519 $('body').append(rejectionReportModal());
2520
2521 $('#tv-rejection-wrapper').addClass('in');
2522 $('#tv-rejection-wrapper, #tv-rejection-modal-close').click(function(){
2523 $('body').removeClass('global-modal-open modal-open');
2524 $('#tv-rejection-wrapper').remove();
2525 });
2526
2527 $('#tv-rejection-wrapper').find('.modal-dialog').click(function(e){
2528 e.stopPropagation();
2529 })
2530
2531 //rid for andy, good candidate to check for a 'low' risk: A3JSJNT0GIBCUH
2532 //rid for lieberman, high risk: A255UD4AY616XX
2533 //selena, high risk & newbie unfriendly: A28QSTY8AH9BJJ
2534 let requester_id = btn.data('requester_id');
2535 let reward = btn.data('reward');
2536 //generate advice for workers based on their approval rate & # of approved hits
2537 let appRange = localStorage.getItem('tv-app-range') || 0;
2538 let appRate = localStorage.getItem('tv-app-rate') || 0;
2539 fetch(`https://view.turkerview.com/v1/requesters/rejections/?requester_id=${requester_id}&reward=${reward}&user_approved=${appRange}&user_rate=${appRate}`, {
2540 method: 'GET',
2541 cache: 'no-cache',
2542 headers: ViewHeaders
2543 }).then(response => {
2544 if (!response.ok) throw response;
2545
2546 return response.json();
2547 }).then(data => {
2548
2549 $('#loading-component').remove();
2550 $('#tv-rejection-wrapper').find('.modal-body').html(getRejectionReport(data));
2551
2552 $('body').addClass('global-modal-open modal-open');
2553
2554
2555 }).catch(ex=> {
2556 $('#loading-component').remove();
2557 $('#tv-rejection-wrapper').find('.modal-body').html(`<div class="alert alert-danger"><h3>Oh No!</h3><p>We weren't able to retrieve data, please close this dialog & try again</p></div>`)
2558 console.log(ex);
2559 });
2560 })
2561}
2562
2563function getRejectionReport(json){
2564 let comment_div = ``;
2565 let stop = 0;
2566 json.comments.forEach(comment => {
2567 if (stop >= 3) return;
2568 stop++;
2569 comment_div += `
2570<div class="row" style="margin-bottom: 0;">
2571 <div class="col-xs-12" style="text-align: left;"><h3 class="text-success">Pros</h3><p class="text-muted">${comment.pros}</p></div>
2572 <div class="col-xs-12" style="text-align: left;"><h3 class="text-danger">Cons</h3><p class="text-muted">${comment.cons}</p></div>
2573 <div class="col-xs-12"><h3 class="text-warning">Rejection Reason</h3><p class="text-muted">${(comment.feedback ? comment.feedback : '-')}</p></div>
2574</div>
2575<hr style="margin-top: 0">
2576 `;
2577 })
2578 return `
2579<!-- Stats -->
2580<div class="row" style="text-align: center;">
2581 <div class="col-xs-12">
2582 <h2 class="text-muted">Rejection Stats</h2>
2583 <h3 style="display: none;">Threat Level</h3>
2584 <h2 style="display: none;" class="${json.recommendation.class}">${json.recommendation.level}</h2>
2585 <h3 style="display: none;">${json.recommendation.reason}</h3>
2586 </div>
2587</div>
2588<div class="row" style="text-align: center;">
2589 <div class="col-xs-4">
2590 <h3>Rejected</h3>
2591 <h2 class="text-danger">${json.total_rejections} (${((json.total_rejections/json.total_reviews)*100).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0})}%)</h2>
2592 </div>
2593 <div class="col-xs-4">
2594 <h3>Unmarked</h3>
2595 <h2 class="text-muted">${json.total_pending} (${((json.total_pending/json.total_reviews)*100).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0})}%) </h2>
2596 </div>
2597 <div class="col-xs-4">
2598 <h3>Approved</h3>
2599 <h2 class="text-success">${json.total_approved} (${((json.total_approved/json.total_reviews)*100).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0})}%)</h2>
2600 </div>
2601</div>
2602<div class="row" style="text-align: center; display: ${(json.inexperienced_rejections + json.experienced_rejections > 0) ? 'block' : 'none'}">
2603 <div class="col-xs-6">
2604 <h3>Inexperienced Worker Rejections</h3>
2605 <h2 class="text-warning">${((json.inexperienced_rejections/(json.experienced_rejections + json.inexperienced_rejections))*100).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0})}%</h2>
2606 </div>
2607 <div class="col-xs-6">
2608 <h3>Experienced Worker Rejections</h3>
2609 <h2 class="text-warning">${((json.experienced_rejections/(json.experienced_rejections + json.inexperienced_rejections))*100).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0})}%</h2>
2610 </div>
2611 <h3 class="text-muted">${json.experience_advice}</h3>
2612</div>
2613<hr style="margin-top: 0">
2614
2615<!-- Impact: We're going to hide this for now until I get feedback on the entire idea. KISS! -->
2616<div class="row" style="text-align: center; display: none;">
2617 <div class="col-xs-12">
2618 <h2 class="text-muted">Rejection Impact (Per Task!)</h2>
2619 <h3>Impact Level</h3>
2620 <h2 class="${json.impact.class}">${json.impact.level}</h2>
2621 <h3>${json.impact.reason}</h3>
2622 </div>
2623</div>
2624<div class="row" style="text-align: center; display: none;">
2625 <div class="col-xs-4">
2626 <h3>Approval Rate</h3>
2627 <h2 class="${json.impact.survey.approval_rate.class}">${json.impact.survey.approval_rate.text}</h2>
2628 </div>
2629 <div class="col-xs-4">
2630 <h3>Time Lost</h3>
2631 <h2 class="${json.impact.survey.time_lost.class}">${json.impact.survey.time_lost.text}</h2>
2632 </div>
2633 <div class="col-xs-4">
2634 <h3>Wages Forfeit</h3>
2635 <h2 class="${json.impact.survey.wages.class}">${json.impact.survey.wages.text}</h2>
2636 </div>
2637</div>
2638<hr style="margin-top: 0; display: none;">
2639
2640
2641<!-- Written Reviews -->
2642<div class="row" style="text-align: center;">
2643 <div class="col-xs-12">
2644 <h2 class="text-muted">Rejection Reviews (Top ${stop})</h2>
2645 <div class="row" style="display: none;"><div class="col-xs-12" style="text-align: left;"><button class="btn btn-success btn-md btn-expand-written-reviews"><i class="fa fa-plus"></i> Show</button></div></div>
2646 <div id="written-rejection-reviews" style="display: block;">
2647 ${comment_div}
2648 </div>
2649 <div class="col-xs-12"><a href="https://turkerview.com/requesters/${json.requester_id}/reviews/rejected" target="_blank">Read More on TurkerView</a></div>
2650 </div>
2651 <script>
2652 $('.btn-expand-written-reviews').click(function(){
2653 if ($(this).children('i').hasClass('fa-plus')){
2654 $(this).html('<i class="fa fa-minus"></i> Hide');
2655 $(this).parent().parent().next().show();
2656 }else{
2657 $(this).html('<i class="fa fa-plus"></i> Show');
2658 $(this).parent().parent().next().hide();
2659 }
2660 });
2661 </script>
2662</div>`;
2663}
2664
2665function rejectionReportModal(){
2666 return `
2667<div id="tv-rejection-wrapper" class="fade">
2668 <div class="modal " style="display: block; z-index: 9999">
2669 <div class="modal-dialog">
2670 <div class="modal-content">
2671 <div class="modal-header">
2672 <button id="tv-rejection-modal-close" type="button" class="close" data-dismiss="modal"><svg class="close-icon" data-reactid=".8.0.0.0.0.0"><g fill="none" stroke="#555" class="close-icon-graphic" stroke-width="2.117" stroke-linecap="round" data-reactid=".8.0.0.0.0.0.0"><path d="M1.2 1.3l7.4 7.5M1.2 8.8l7.4-7.5" data-reactid=".8.0.0.0.0.0.0.0"></path></g></svg></button>
2673 <h2 class="modal-title">TurkerView Rejection Report</h2>
2674 </div>
2675 <div class="modal-body">
2676
2677 <div id="loading-component" class="row" style="text-align: center;">
2678 <div class="col-xs-12" style="min-height: 100px;">
2679 <div class="loading"></div>
2680 </div>
2681 <h3>Loading Report...</h3>
2682 </div>
2683
2684 </div>
2685 </div>
2686 </div>
2687 </div>
2688 <div id="tv-rejection-modal-backdrop" class="modal-backdrop fade in"></div>
2689</div>
2690
2691
2692`;
2693}
2694
2695function buttonHitUp(hitData, react){
2696 //lets lock ourselves into the desktop mode and see if anyone complains about TVJS not working on mobile
2697 $('.desktop-row').each(function(rowNum){
2698 let thisRow = $(this);
2699
2700 let rid = !react[rowNum].project ? react[rowNum].requester_id : react[rowNum].project.requester_id;
2701 let reward = !react[rowNum].project ? react[rowNum].monetary_reward.amount_in_dollars.toFixed(2) : react[rowNum].project.monetary_reward.amount_in_dollars.toFixed(2);
2702 let title = !react[rowNum].project ? react[rowNum].title : react[rowNum].project.title;
2703
2704 //hit data
2705 if (hitData['error']) {
2706 thisRow.find('.project-name-column').prepend(`<span class="turkerview tv-tooltip" data-toggle="tooltip" title="You must be logged in on TurkerView.com to use this feature" style="color: #fff; background-color: rgba(128, 128, 128, 0.25);"><i class="fa fa-exclamation"></i></span>`);
2707 } else {
2708 let thisKey = rid.concat(reward, title)
2709 let avg_completion = hitData[thisKey] ? hitData[thisKey].avg_completion : null;
2710 let avg_hourly = avg_completion !== null ? ((3600/avg_completion)*reward).toFixed(2) : null;
2711 let min_completion = hitData[thisKey] ? hitData[thisKey].min_completion : null;
2712 let min_hourly = min_completion !== null ? ((3600/min_completion)*reward).toFixed(2) : null;
2713 let max_completion = hitData[thisKey] ? hitData[thisKey].max_completion : null;
2714 let max_hourly = max_completion !== null ? ((3600/max_completion)*reward).toFixed(2) : null;
2715
2716 let hitPay = hitData[thisKey] ? payFormat(hitData[thisKey].avg_pay) : payFormat(null);
2717 let hitReviewCount = hitData[thisKey] ? hitData[thisKey].total_reviews : '0';
2718
2719 let hitColor = avg_completion !== null ? hourlyFormat((3600/avg_completion)*reward) : 'rgba(128, 128, 128, ';
2720 let hitIconUrl = avg_completion !== null ? iconImage((3600/avg_completion)*reward) : 'https://turkerview.com/assets/images/tv-unrated.png';
2721 let hitOpacity = hitData[thisKey] ? confidence(hitData[thisKey].total_reviews) : '0.5';
2722
2723 thisRow.children('.project-name-column').find('.turkerview').css('background-image', `url(${hitIconUrl})`).css('opacity', `${hitOpacity}`);
2724
2725 var popoverHITHTML = buildHITPopover(hitColor, avg_completion, avg_hourly, min_completion, min_hourly, max_completion, max_hourly, hitPay, hitReviewCount);
2726 thisRow.find('.project-name-column > .tv-container').append(popoverHITHTML);
2727 }
2728
2729 //export button
2730 thisRow.find('.project-name-column').append(`<span class="turkerview tv-tooltip tv-export text-muted" data-toggle="tooltip" title="Export this HIT to TurkerView Forum!" data-rowNum="${rowNum}" style="cursor: pointer;"><i class="fa fa-external-link text-muted"></i></span>`);
2731
2732 }); //end row loop
2733
2734 //ugly hack to get around sandboxing
2735 location.assign("javascript:$('.tv-tooltip').tooltip();void(0)");
2736}
2737
2738function buildDefaultPopover(requester_id){
2739 let popOverHTML = `
2740<div class="tv-popover tv-hide tv-popover-placeholder" title="">
2741 <h3>Pardon Our Dust</h3>
2742 <div class="col-xs-12" style="padding: 8px;">
2743 <p>Unfortunately TurkerViewJS couldn't retrieve data from the server, this may happen occasionally as we move the API. Please consider checking the website to see the Requester profile!</p>
2744 <p><a href="https://turkerview.com/requesters/${requester_id}" target="_blank">Requester Profile Link</a></p>
2745 <div>
2746</div>
2747
2748 </div>
2749</div>
2750`;
2751 return popOverHTML;
2752}
2753
2754function buildVIEWPopover(params){
2755 let popOverHTML = `
2756<div class="tv-popover tv-hide" title="">
2757 <h3>TurkerView Ratings (${params.reviewCount})</h3>
2758 <h4 style=" text-align: center; margin-top: 10px; margin-bottom: 0;"><a href="https://turkerview.com/requesters/${params.rid}-${slugify(params.rname)}" target="_blank">${params.rname}</a></h4>
2759 <div style="padding: 9px 14px;">
2760 <div style="display: flex;">
2761 <div style="flex: 1;">
2762 <span class="tv-td">Pay Rate:</span>
2763 <span class="tv-td" style="padding: 5px 0;">Pay Sentiment:</span>
2764 <span class="tv-td">Approval:</span>
2765 <span class="tv-td">Communication:</span>
2766 <span class="tv-td" style="text-align: center; padding: 10px 0;">${params.rejections}</span>
2767 <span class="tv-td" style="text-align: center; padding: 5px 0;"><a href="https://turkerview.com/requesters/${params.rid}-${slugify(params.rname)}" target="_blank">Overview</a></span>
2768 </div>
2769 <div style="flex: 1;">
2770 <span class="tv-td">${params.hourly}</span>
2771 <span class="tv-td" style="padding: 5px 0;">${params.pay}</span>
2772 <span class="tv-td">${params.approval}</span>
2773 <span class="tv-td">${params.comm}</span>
2774 <span class="tv-td" style="text-align: center; padding: 10px 0;">${params.blocks}</span>
2775 <span class="tv-td" style="text-align: center; padding: 5px 0;"><a href="https://turkerview.com/requesters/${params.rid}-${slugify(params.rname)}/reviews" target="_blank">Reviews</a></span>
2776 </div>
2777 </div>
2778 </div>
2779 <h3>MTurk (${params.requester_info == null ? "N/A" : params.requester_info.activityLevel + " Activity"})</h3>
2780 <div style="padding: 9px 14px;">
2781 <div style="display: flex;">
2782 <div style="flex: 1;">
2783 <span class="tv-td">Approval %:</span>
2784 <span class="tv-td">Approval Time:</span>
2785 </div>
2786 <div style="flex: 1;">
2787 <span class="tv-td">${params.requester_info == null ? "N/A In Queue" : params.requester_info.taskApprovalRate}</span>
2788 <span class="tv-td" style="padding: 5px 0;">${params.requester_info == null ? "N/A In Queue" : params.requester_info.taskReviewTime}</span>
2789 </div>
2790 </div>
2791 </div>
2792 <h3>Your Review Ratings (${params.userReviewCount})</h3>
2793 <div style="padding: 9px 14px;">
2794 ${params.user_data.html_template}
2795 </div>
2796</div>
2797`;
2798 return popOverHTML;
2799}
2800
2801function buildHITPopover(hitColor, avg_completion, avg_hourly, min_completion, min_hourly, max_completion, max_hourly, hitPay, hitReviewCount){
2802 let popOverHTML = `
2803<div class="tv-popover tv-hide" title="">
2804 <h3>TurkerView HIT Ratings (${hitReviewCount})</h3>
2805 <div style="padding: 9px 14px;">
2806 <span class="tv-td" style="text-align: center; font-size: 1.25rem;"><span style="color: ${hitColor} 1);">$${avg_hourly}</span><span style="font-size: 50%;"> / avg hr
2807 <br><span class="text-muted">${new Date(1000 * avg_completion).toISOString().substr(11, 8)} avg time</span></span></span>
2808<div style="display: flex; text-align: center;">
2809<div style="flex: 1;">
2810<span class="tv-td" style="padding: 5px 0; font-size: 1rem;"><i class="fa fa-caret-down" style="color: red;"></i> $${max_hourly}<span style="font-size: 60%;"> / hr
2811<br><span class="text-muted">${new Date(1000 * max_completion).toISOString().substr(11, 8)}
2812<br><span class="text-muted">(lowest)</span></span></span>
2813</div>
2814<div style="flex: 1;">
2815<span class="tv-td" style="padding: 5px 0; font-size: 1rem;"><i class="fa fa-caret-up" style="color: green;"></i> $${min_hourly}<span style="font-size: 60%;"> / hr
2816<br><span class="text-muted">${new Date(1000 * min_completion).toISOString().substr(11, 8)}
2817<br><span class="text-muted">(highest)</span></span></span>
2818</div>
2819</div>
2820
2821 </div>
2822</div>
2823`;
2824 return popOverHTML;
2825}
2826
2827/* Export HITs to TVF */
2828$(document).on('click', '.tv-export', function(){
2829 let that = $(this);
2830 $(this).children('i').removeClass('fa-external-link').addClass('fa-spinner fa-pulse');
2831 let rowNum = $(this).data('rownum');
2832 let hitKey = !react[rowNum].project ? react[rowNum].requester_id.concat(react[rowNum].monetary_reward.amount_in_dollars, react[rowNum].title) : react[rowNum].project.requester_id.concat(react[rowNum].project.monetary_reward.amount_in_dollars, react[rowNum].project.title);
2833
2834 let view;
2835 if (queue.test(windowHREF)){
2836 view = viewData.requesters[react[rowNum].project.requester_id]
2837 } else {
2838 view = viewData.requesters[react[rowNum].requester_id];
2839 }
2840
2841 let hit = hitData[hitKey];
2842 let hitDataOMFG = !react[rowNum].project ? react[rowNum] : react[rowNum].project;
2843
2844
2845 $.ajax({
2846 type: 'POST',
2847 url: 'https://forum.turkerview.com/export.php',
2848 data: {
2849 hitData: hitDataOMFG,
2850 reqTV: view,
2851 hitTV: hit
2852 },
2853 xhrFields: {
2854 withCredentials: true
2855 }
2856 }).done(function(data){
2857 that.children('i').removeClass('fa-spinner fa-pulse').addClass('fa-check');
2858 that.attr('data-original-title', 'Thanks for Sharing!');
2859 });
2860});
2861
2862function fillTable(){
2863 if (tvAgreement == false){
2864 if ($('#tv-agreement').length) return;
2865 $('#tv-table').hide();
2866 $('#tv-table').parent().prepend(`
2867<form id="tv-agreement">
2868 <h2>TurkerView Intro</h2>
2869 <p>Thank you for joining the TurkerView community!</p>
2870 <p>We're a little different from other platforms and above all else we're about <strong class="text-success">helping workers earn more on MTurk</strong>, so we aim to highlight positive work experiences in reviews. That doesn't mean you can't review the junk too, but workers should keep a balanced perspective of their work profile on the platform.</p>
2871 <p>For convenience, we've made a very short (no ads, no wasted time) overview video of how the script works & how a review can be submitted</p>
2872 <p style="text-align: center">
2873 <video width="70%" controls="">
2874 <source src="https://turkerview.com/assets/TurkerViewJS.mp4" type="video/mp4">
2875 Your browser does not support the video tag.
2876 </video>
2877 </p>
2878<h2>TurkerView Participation Guidelines</h2>
2879<ul class="fa-ul">
2880 <li>
2881 <i class="fa-li fa fa-times-rectangle-o"></i><strong> Do <u class="text-danger">not</u> leave reviews out of frustration</strong>
2882 <p style="padding-left: 10px; padding-right: 10px; text-align: justify;">Especially over a rejection. Reviews should aim to be matter-of-fact as much as possible. Do feel free
2883 to use <a href="https://forum.turkerview.com/forums/daily-mturk-hits-threads.2/" target="_blank" style="text-decoration: underline;">TurkerView Forum</a> as a watercooler vent area, but think of reviews as an e-mail to your coworker that your boss can read. This means no inflammatory speech, including accusing someone of "scamming." Many negative experiences are a result of miscommunication, not ill intent, and will be cleared up with a quick e-mail.
2884 <br>Remember, the MTurk participation agreement requires <em>"As a Worker, you agree that: (i) you will interact with Requesters in a professional and courteous manner"</em>
2885 <br>It applies to TurkerView as well.
2886 </p>
2887 </li>
2888 <li>
2889 <i class="fa-li fa fa-times-rectangle-o"></i><strong> Do <u class="text-danger">not</u> rate pay sentiment drastically different from the hourly guidelines</strong>
2890 <p style="padding-left: 10px; padding-right: 10px; text-align: justify;"><em>(e.g., great pay for $2/hr or "underpaid" for $100/hr)</em> unless you give clear indications why in the comments so folks can understand
2891 why you feel that way.</p>
2892 </li>
2893 <li>
2894 <i class="fa-li fa fa-times-rectangle-o"></i><strong> Do <u class="text-danger">not</u> rate communication unless you've contacted the requester</strong>
2895 <p style="padding-left: 10px; padding-right: 10px; text-align: justify;">Workers rely on accurate & sensible information from our services. Requesters should generally be given 2-3 business days to reply & workers are asked to explain their communication experience with the requester.
2896 </p>
2897 </li>
2898 <h3>...And Finally</h3>
2899 <li>
2900 <i class="fa-li fa fa-check-square-o"></i><strong> <u class="text-success">Do</u> adjust your times & check your hourly is accurate <em>before</em> submitting a review</strong>
2901 <p style="padding-left: 10px; padding-right: 10px; text-align: justify;">If you took a break to stretch, answer the phone, or do anything not related to the HIT <em>(including things like leaving optional feedback for a survey)</em> <strong>pause the hourly tracker</strong> in the HIT window. Otherwise it gives an inaccurate portrayal of the actual work to others
2902 and can hurt a Requester's research/profile on the platform. Also, while we're constantly working to improve TurkerView, the script isn't perfect and can sometimes log the time incorrectly. If the completion time looks drastically off and you can't recall the time accurately, please don't submit a review for that HIT. In this case, no data is better than bad data.</p>
2903 </li>
2904</ul>
2905<p style="text-align: right; margin-bottom: 0;"><button type="submit" id="tv-agree-btn" class="btn btn-primary">Got it! Let me in!</button></p>
2906<p style="text-align: right;"><small><span class="text-muted">Don't worry, we'll only ask you to do this once!</span></small></p>
2907</form>`);
2908
2909 $('#tv-agree-btn').on('click', function(){
2910 $('#tv-agreement').remove();
2911 tvAgreement = true;
2912 localStorage.setItem('tv-agree', true);
2913 fillTable();
2914 });
2915 return;
2916 }
2917
2918 $('#tv-table > tbody').html('');
2919 $('#tv-table').show();
2920
2921
2922
2923 let retrievalDate = $('#tvDateSelection').val();
2924 Object.keys(localStorage)
2925 .forEach(function(key){
2926 if (/^tv_/.test(key)) {
2927 let json = JSON.parse(localStorage.getItem(key));
2928 let sizeEst = (JSON.stringify(json).length*16)/(8*1024);
2929 /* Let's clean up old data or data we no longer have use for so we can keep localstorage clean for the user */
2930 /*
2931 Single record HITs are <400 len, ~.75kb in size so we could store up to 6,000 without overflowing localstorage, we should never get close to that.
2932 A massive Forker / Overwatch / etc install complicates this immensely, so lets be stingy with what TV is storing to avoid any possible complications while still getting good AA data
2933 */
2934 let days = moment(today).tz('America/Los_Angeles', true).diff(moment(json['date']).tz('America/Los_Angeles', true), 'days');
2935
2936 if (days > 4 && !json.reviewId){
2937 //This record is too old to be reviewed & doesn't have a review id, its safe to remove it
2938 localStorage.removeItem(key);
2939 return;
2940 } else if (days > 3 && json.reviewId && json.fast){
2941 //This record is too old to be edited from TVJS, it has been reviewed & we already uploaded the approval time no need to keep it
2942 localStorage.removeItem(key);
2943 return;
2944 } else if (days > 10 && json.reviewId && sizeEst > 5){
2945 //This record is simply too big to keep longer than 10 days, this should be incredibly rare to invoke, but adds a very important safety net around the user's limited local storage length
2946 localStorage.removeItem(key);
2947 return;
2948 } else if (days > 4 && !json.times){
2949 //This record is from prior to TVJS10, we should remove it
2950 localStorage.removeItem(key);
2951 return;
2952 } else if (days > 31){
2953 //Its been too long, lets move on with our lives.
2954 localStorage.removeItem(key);
2955 return;
2956 }
2957
2958 if (json['date'] !== retrievalDate) return;
2959
2960 let mind;
2961 if (json['multi'] == true && json['times']){
2962 let arr = json['times'];
2963 mind = Math.floor(median(filterOutliers(arr))/1000);
2964 if (!mind) mind = Math.floor(median(arr)/1000);
2965 } else mind = Math.floor(json['completionTime'] / 1000);
2966
2967 let hourly = 3600 / mind * json['reward'];
2968 let disabled = json['reviewId'] != null ? 'disabled' : '';
2969
2970
2971 let status_indicator = `<i class="fa fa-clock-o text-muted" style="cursor: default;" data-toggle="tooltip" data-title="HIT Pending"></i>`;
2972 if (json['hit_status'] == 1) status_indicator = `<i class="fa fa-check text-success" style="cursor: default;" data-toggle="tooltip" data-title="HIT Approved"></i>`;
2973 else if (json['hit_status'] == -1) status_indicator = `<i class="fa fa-times text-danger" style="cursor: default;" data-toggle="tooltip" data-title="HIT Rejected"></i>`;
2974
2975
2976 if (json['display'] === false){
2977 //user removed the record, don't display anymore.
2978 }
2979 else if (json['reviewId'] == null){
2980
2981 let base_hourly = (3600/(json['completionTime']/1000))*json['reward'];
2982 let eligible = (base_hourly < 5 || base_hourly > 50 || settings.enable_quick_reviews != true) ? false : true;
2983
2984 let quick_html = `
2985<div class="btn-group dropdown show">
2986 <a style="${ eligible ? 'border-color: #a56914' : ''}" class="${ eligible ? 'btn btn-primary btn-quick-review' : 'btn btn-default disabled'} btn-sm" data-hitKey="${escape(key)}" data-toggle="tooltip" data-title="This will submit your review to TV with no pay sentiment rating or written text.">Quick Review</a>
2987 <a style="${ eligible ? 'border-color: #a56914' : ''}" class="${ eligible ? 'btn btn-primary btn-quick-review-dropdown' : 'btn btn-default disabled'} btn-sm dropdown-toggle" role="button" data-toggle="tooltip" data-title="Quick review with pay sentiment"><i class="fa fa-thumbs-up"></i></a>
2988 <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
2989 <a class="dropdown-item ${base_hourly < 12 ? 'text-muted disabled' : ' text-success'} btn-quick-sentiment" href="#" data-hitKey="${escape(key)}" data-sentiment="5"><i class="fa fa-thumbs-up"></i> Generous</a>
2990 <a class="dropdown-item ${base_hourly < 10.50 ? 'text-muted disabled' : ' text-success'} btn-quick-sentiment" href="#" data-hitKey="${escape(key)}" data-sentiment="4"><i class="fa fa-thumbs-up"></i> Good</a>
2991 <a class="dropdown-item ${base_hourly < 7.25 ? 'text-muted disabled' : ' text-warning'} btn-quick-sentiment" href="#" data-hitKey="${escape(key)}" data-sentiment="3"><i class="fa fa-thumbs-up"></i> Fair</a>
2992 <a class="dropdown-item ${base_hourly > 10.50 ? 'text-muted disabled' : ' text-danger'} btn-quick-sentiment" href="#" data-hitKey="${escape(key)}" data-sentiment="2"><i class="fa fa-thumbs-up"></i> Low</a>
2993 <a class="dropdown-item ${base_hourly > 7.25 ? 'text-muted disabled' : ' text-danger'} btn-quick-sentiment" href="#" data-hitKey="${escape(key)}" data-sentiment="1"><i class="fa fa-thumbs-up"></i> Underpaid</a>
2994 </div>
2995</div>`;
2996
2997 $('#tv-table > tbody').append(`
2998<tr>
2999<td class="col-xs-3" style="overflow: hidden; white-space: nowrap; max-width: 140px;"><a href="https://worker.mturk.com/contact_requester/hit_type_messages/new?hit_type_message[hit_type_id]=TURKERVIEW&hit_type_message[requester_id]=${json['rid']}&hit_type_message[requester_name]=${json['requester']}" target="_blank"><i class="fa fa-fw fa-envelope-o" data-toggle="tooltip" title="Contact Requester"></i></a> ${json['requester']}</td>
3000<td class="col-xs-4" style="overflow: hidden; white-space: nowrap; max-width: 400px;">${json['title']}</td>
3001<td class="col-xs-1" style="overflow: hidden; white-space: nowrap; text-align: right;">$${hourly.toFixed(2)}</td>
3002<td class="col-xs-1" style="overflow: hidden; white-space: nowrap; text-align: center;">${status_indicator}</td>
3003<td class="col-xs-3" style="overflow-x: hidden; overflow-y: visible; overflow: visible; white-space: nowrap; max-height: 38px; text-align: center;">
3004 <a style="border-color: #a56914" class="btn btn-primary btn-sm btn-review ${disabled}" data-toggle="tooltip" data-title="Leave a review on TurkerView!" data-hitKey="${escape(key)}">Review</a>
3005 ${quick_html}
3006 <a class="btn btn-danger btn-sm btn-tv-remove-review" data-hitKey="${escape(key)}" style="border-color: #952d2b; margin-left: 8px;" data-toggle="tooltip" data-title="Remove Row (This will prevent you from being able to review this HIT)">X</a>
3007</td>
3008</tr>`)
3009 } else if (json['reviewId'] != null){
3010 $('#tv-table > tbody').append(`
3011<tr ${hideReviewedFromTable ? 'style="display: none;"' : ''}>
3012<td class="col-xs-3" style="overflow: hidden; white-space: nowrap; max-width: 140px;"><a href="https://worker.mturk.com/contact_requester/hit_type_messages/new?hit_type_message[hit_type_id]=TURKERVIEW&hit_type_message[requester_id]=${json['rid']}&hit_type_message[requester_name]=${json['requester']}" target="_blank"><i class="fa fa-fw fa-envelope-o" data-toggle="tooltip" title="Contact Requester"></i></a> ${json['requester']}</td>
3013<td class="col-xs-4" style="overflow: hidden; white-space: nowrap; max-width: 400px;">${json['title']}</td>
3014<td class="col-xs-1" style="overflow: hidden; white-space: nowrap; text-align: right;">$${hourly.toFixed(2)}</td>
3015<td class="col-xs-1" style="overflow: hidden; white-space: nowrap; text-align: center;">${status_indicator}</td>
3016<td class="col-xs-3" style="overflow: hidden; white-space: nowrap; max-height: 38px; text-align: center;">
3017 <a href="https://turkerview.com/reviews/edit.php?id=${json['reviewId']}" target="_blank" class="btn btn-default btn-sm" data-hitKey="${escape(key)}" data-toggle="tooltip" data-title="Edit Review (Takes you to TurkerView)">Edit</a>
3018 <a class="btn btn-danger btn-sm btn-tv-remove-review pull-right" data-hitKey="${escape(key)}" style="border-color: #952d2b; margin-left: 8px;" data-toggle="tooltip" data-title="Remove Row (This will prevent you from being able to review this HIT)">X</a>
3019</td>
3020</tr>`)
3021 }
3022 }
3023 });
3024
3025 location.assign("javascript:$('.btn-review').tooltip();javascript:$('.btn-tv-remove-review').tooltip();$('i').tooltip();$('.btn').tooltip();void(0)");
3026
3027 $('.btn-quick-review, .btn-quick-sentiment').click(function(){
3028 //clear old error/success message
3029 //pull data, submit it immediately
3030 //disable other buttons while submitting
3031 //place error/success message directly to dash
3032
3033
3034 location.assign(`javascript:$('[data-toggle="tooltip"]').tooltip('hide');void(0);`);
3035 let td = $(this).closest('td');
3036 $('#new-zzzreview').find('.btn').addClass('disabled');
3037
3038
3039 let hitKey = unescape($(this).data('hitkey'));
3040
3041 let appRange = localStorage.getItem('tv-app-range') || null;
3042 let appRate = localStorage.getItem('tv-app-rate') || null;
3043 if (appRange == null || appRate == null){
3044 checkQual('get');
3045 alert('Script was missing data, please try again!');
3046 return;
3047 }
3048
3049 let hitData = JSON.parse(localStorage.getItem(hitKey));
3050 let requester = hitData['requester'];
3051 let rid = hitData['rid'];
3052 let title = hitData['title'];
3053 let hit_set_id = hitData['hit_set_id'] ? hitData['hit_set_id'] : hitData['gid'];
3054 let reward = hitData['reward'];
3055 let submit_date = hitData['date'];
3056
3057 let mind;
3058 /* Batch results*/
3059 if (hitData['multi'] == true && hitData['times']){
3060 let arr = hitData['times'];
3061 mind = Math.floor(median(filterOutliers(arr))/1000);
3062 if (!mind) mind = Math.floor(median(arr)/1000);
3063 } else mind = Math.floor(hitData['completionTime'] / 1000);
3064
3065 let minutes = Math.floor(mind / 60);
3066 let seconds = mind % 60;
3067 let submitTime = hitData['submitTime'];
3068
3069 let now = moment.tz('America/Los_Angeles');
3070
3071 t_approved = 0;
3072 if (hitData['approved']) t_approved = hitData['approved'];
3073 else if (hitData['hit_status'] == 1 && hitData['task_count'] == 1) t_approved = 1;
3074
3075 t_rejected = 0;
3076 if (hitData['rejected'] && hitData['task_count'] > 1) t_rejected = hitData['rejected'];
3077
3078 let reviewObject = {
3079 'hitKey': hitKey,
3080 'requester_name': requester,
3081 'requester_id': rid,
3082 'hit_title': title,
3083 'hit_set_id': hit_set_id,
3084 'base_pay': reward,
3085 'minutes': minutes,
3086 'seconds': seconds,
3087 'app_range': appRange,
3088 'app_rate': appRate,
3089 'tasks_completed': hitData['task_count'],
3090 'tasks_approved': t_approved,
3091 'tasks_rejected': t_rejected,
3092 'hit_status': hitData['hit_status'] ? hitData['hit_status'] : 0,
3093 'tracked_completion': mind,
3094 'submit_date': submit_date,
3095 'no_share': 1
3096 };
3097
3098 if (hitData['fast']) reviewObject.approval_time = hitData['fast'];
3099 if (hitData['hit_status'] && hitData['hit_status'] == -1) reviewObject.requester_feedback = hitData['feedback'];
3100 if ($(this).hasClass('btn-quick-sentiment')) reviewObject.pay_rating = $(this).data('sentiment');
3101
3102 var form_data = new FormData();
3103 for ( var key in reviewObject ) {
3104 form_data.append(key, reviewObject[key]);
3105 }
3106
3107 fetch('https://turkerview.com/api/v2/reviews/submit/', {
3108 method: 'POST',
3109 body: form_data,
3110 headers: ViewHeaders
3111 }).then(response => {
3112 if (!response.ok) throw response;
3113
3114 return response.json();
3115 }).then(response => {
3116 //console.log(JSON.stringify(response));
3117 if (response.status == 'ok'){
3118
3119
3120 console.log(response);
3121 console.log(response.postData);
3122
3123
3124 td.html(`<i class="fa fa-check text-success"></i>`);
3125 $('#new-zzzreview').find('.btn-quick-review, .btn-review, .btn-quick-review-dropdown, .btn-tv-remove-review, .btn:contains(Edit)').removeClass('disabled');
3126
3127 /*
3128 const noticeAlert = document.getElementById('review-alert');
3129 noticeAlert.classList.remove('alert-success');
3130 noticeAlert.classList.remove('alert-warning');
3131 noticeAlert.classList.remove('alert-danger');
3132 noticeAlert.classList.add('alert-'+response.class);
3133 noticeAlert.style.display = 'block';
3134 noticeAlert.classList.remove('hide');
3135 noticeAlert.innerHTML = response.html;
3136 */
3137
3138
3139 let reviewed = JSON.parse(localStorage.getItem(hitKey));
3140 console.log(reviewed);
3141 reviewed['reviewed'] = true;
3142 reviewed['reviewId'] = response.new_review_id;
3143 localStorage.setItem(hitKey, JSON.stringify(reviewed));
3144 } else{
3145 console.log(response);
3146 console.log(response.postData);
3147
3148 const noticeAlert = document.getElementById('review-alert');
3149 noticeAlert.classList.remove('alert-success');
3150 noticeAlert.classList.remove('alert-warning');
3151 noticeAlert.classList.remove('alert-danger');
3152 noticeAlert.classList.add('alert-'+response.class);
3153 noticeAlert.classList.remove('hide');
3154 noticeAlert.style.display = 'block';
3155 noticeAlert.innerHTML = response.html;
3156
3157
3158 $('#new-zzzreview').find('.btn-quick-review, .btn-review, .btn-quick-review-dropdown, .btn-tv-remove-review, .btn:contains(Edit)').removeClass('disabled');
3159
3160 $(window).scrollTop(0)
3161 }
3162
3163 }).catch(exception => {
3164 console.log(exception);
3165 const noticeAlert = document.getElementById('review-alert');
3166 noticeAlert.classList.remove('alert-success');
3167 noticeAlert.classList.remove('alert-warning');
3168 noticeAlert.classList.remove('alert-danger');
3169 noticeAlert.classList.add('alert-danger');
3170 noticeAlert.classList.remove('hide');
3171 noticeAlert.style.display = 'block';
3172 noticeAlert.innerHTML = `<div class="alert alert-danger"><h3>That's not good!</h3><p>We hit a major error that isn't handled by the server, please send details to CT:</p><p>${exception}</p></div>`;
3173 window.scrollTo(0,0);
3174 //$('#tvModal').scrollTop(0)
3175 });
3176
3177
3178
3179 });
3180
3181 $('.btn-quick-review-dropdown').click(function(){
3182 let dropdown = $(this).next('.dropdown-menu');
3183 $('.dropdown-menu').css('display', 'none');
3184 if (dropdown.is(':visible')) dropdown.css('display', 'none');
3185 else dropdown.css('display', 'block');
3186 //$(this).next('.dropdown-menu').css('display', 'block');
3187 });
3188
3189 $('input[name=tvHideReviewed]').on('click', function(){
3190 hideReviewedFromTable = $('input[name=tvHideReviewed]').is(':checked') ? true : false;
3191
3192 localStorage.setItem('tv-hide-reviewed', hideReviewedFromTable)
3193 if (hideReviewedFromTable){
3194 $('#tv-table > tbody > tr').each(function(){
3195 let review_box_text = $(this).children('td:eq(4)').text();
3196 if (review_box_text.indexOf('Edit') > -1) $(this).hide();
3197 });
3198 } else {
3199 $('#tv-table > tbody > tr').each(function(){
3200 $(this).show();
3201 });
3202 }
3203 });
3204
3205
3206
3207 $('.btn-tv-remove-review').on('click', function(){
3208 let hitKey = unescape($(this).data('hitkey'));
3209 let json = JSON.parse(localStorage.getItem(hitKey));
3210 json.display = false;
3211 localStorage.setItem(hitKey, JSON.stringify(json));
3212 $('.tooltip').remove();
3213 $(this).closest('tr').remove();
3214 });
3215
3216 $('.btn-review').on('click', function(){
3217 $('form#new-review').trigger('reset');
3218 $('input[name=pay_rating], input[name=comm_rating], input[name=hit_status], input[name=approval_time], input[name=requester_feedback]').removeAttr('value');
3219 $('#review-failure').hide();
3220 $('#hit_status_display').html(``);
3221 $('.btn-pay, .btn-comm, .btn-status').removeClass('active');
3222 $('.btn-pay, .btn-comm, .btn-status').find('i').remove();
3223 $('#wage_est').text('');
3224 $('#wage_est').hide();
3225
3226 let appRange = localStorage.getItem('tv-app-range') || null;
3227 let appRate = localStorage.getItem('tv-app-rate') || null;
3228 if (appRange == null || appRate == null){
3229 checkQual('get');
3230 alert('Script was missing data, please try again!');
3231 return;
3232 }
3233 let hitKey = unescape($(this).data('hitkey'));
3234
3235
3236
3237 let hitData = JSON.parse(localStorage.getItem(hitKey));
3238
3239 if (hitData['contacted']){
3240 $('#comm-rating-row').show();
3241 } else{
3242 $('#comm-rating-row').hide();
3243 }
3244
3245 let requester = hitData['requester'];
3246 let rid = hitData['rid'];
3247 let title = hitData['title'];
3248 let hit_set_id = hitData['hit_set_id'] ? hitData['hit_set_id'] : hitData['gid'];
3249 let reward = hitData['reward'];
3250
3251 let mind;
3252 /* Batch results*/
3253 if (hitData['multi'] == true && hitData['times']){
3254 let arr = hitData['times'];
3255 mind = Math.floor(median(filterOutliers(arr))/1000);
3256 if (!mind) mind = Math.floor(median(arr)/1000);
3257 } else mind = Math.floor(hitData['completionTime'] / 1000);
3258
3259 let minutes = Math.floor(mind / 60);
3260 let seconds = mind % 60;
3261 let submitTime = hitData['submitTime'];
3262
3263 let now = moment.tz('America/Los_Angeles');
3264 let ms = moment(now).diff(moment(submitTime));
3265
3266 t_approved = 0;
3267 if (hitData['approved']) t_approved = hitData['approved'];
3268 else if (hitData['hit_status'] == 1 && hitData['task_count'] == 1) t_approved = 1;
3269
3270 t_rejected = 0;
3271 if (hitData['rejected'] && hitData['task_count'] > 1) t_rejected = hitData['rejected'];
3272
3273 $('input[name=hitKey]').val(hitKey);
3274 $('input[name=req_name]').val(requester).attr('readonly', true);
3275 $('input[name=req_id]').val(rid).attr('readonly', true);
3276 $('input[name=hit_title]').val(title).attr('readonly', true);
3277 $('input[name=group_id]').val(hit_set_id).attr('readonly', true);
3278 $('input[name=base_pay]').val(reward).attr('readonly', true);
3279 $('input[name=minutes]').val(minutes);
3280 $('input[name=seconds]').val(seconds);
3281 $('input[name=app_range]').val(appRange);
3282 $('input[name=app_rate]').val(appRate);
3283 $('input[name=tasks_completed]').val(hitData['task_count']);
3284 $('input[name=tracked_completion]').val(mind);
3285 $('input[name=submit_date]').val(hitData['date']);
3286 $('input[name=tasks_approved]').val(t_approved);
3287 $('input[name=tasks_rejected]').val(t_rejected);
3288 $('input[name=hit_status]').val((hitData['hit_status'] ? hitData['hit_status'] : 0));
3289 if (hitData['fast']) $('input[name=approval_time]').val(hitData['fast']);
3290 if (hitData['hit_status'] && hitData['hit_status'] == -1) $('input[name=requester_feedback]').val(hitData['feedback']);
3291
3292 if (hitData['hit_status'] && hitData['hit_status'] == -1) $('#hit_status_display').html(`<p><span class="text-danger"><i class="fa fa-times"></i> Rejected</span><br><strong>Reason:</strong> ${hitData['feedback']}</p>`);
3293 else if (hitData['hit_status'] && hitData['hit_status'] == 1 && hitData['task_count'] == 1) $('#hit_status_display').html(`<p class="text-success"><i class="fa fa-check"></i> Approved</p>`);
3294 else if (hitData['rejected'] && hitData['rejected'] == hitData['task_count']) $('#hit_status_display').html(`<p><span class="text-danger"><i class="fa fa-times"></i> All Rejected</span><br><strong>Reason:</strong> ${hitData['feedback']}</p>`);
3295 else if (hitData['rejected'] && hitData['rejected'] < hitData['task_count'] && hitData['approved']) $('#hit_status_display').html(`<p><span class="text-warning"><i class="fa fa-times"></i> Some Rejected</span><br><strong>Reason:</strong> ${hitData['feedback']}</p>`);
3296 else $('#hit_status_display').html(`<p class="text-muted"><i class="fa fa-clock-o"></i> Pending</p>`);
3297
3298
3299 if (hitData['task_count'] > 2) $('#batch_alert').removeClass('hidden');
3300 else $('#batch_alert').addClass('hidden');
3301
3302 $('#tvDashModal, #tv-dash-modal-backdrop').hide();
3303 $('#tvModal, #tv-modal-backdrop').show();
3304 $('body').addClass('global-modal-open modal-open');
3305
3306 wageEst();
3307 })
3308}
3309
3310function dupCheck(){
3311 let requester = $('#req_id').val();
3312 let title = $('#hit_title').val();
3313 let reward = $('#base_pay').val();
3314 $.get(`https://turkerview.com/common/dup-check.php?check_requester=${requester}&check_title=${title}&check_reward=${reward}`).done(function(response){
3315 if (response.indexOf('none') == -1){
3316 $('#duplicate-alert').children('p').html(`<form action="https://turkerview.com/reviews/edit.php" method="post" style="display: inline;"><input type="hidden" name="id" value="${response}">Does this look familiar to you? It seems you've reviewed this HIT & Requester before, you should <a id="dup-edit" href="javascript:;" onclick="parentNode.submit();">edit your previous review</a> instead!</form>`);
3317 $('#duplicate-alert').removeClass('hidden');
3318 }
3319 })
3320}
3321
3322function tvApiAlert(){ return `
3323<div id="dash-api-change-alert" class="alert alert-success" style="${(settings.tv_api_key == null || settings.tv_api_key == '' || settings.tv_api_key.length != 40 ? '' : 'display: none;')}">
3324 <h3>TurkerView's API Is Changing</h3>
3325 <p>Sorry for the intrusion, but we're expanding our services & infrastructure and making huge improvements to the way we deliver information & data to Turkers in 2019!</p>
3326 <p>TVJS 10 is out! You can read change details <a href="https://forum.turkerview.com/threads/turkerviewjs-10.2010/" target="_blank">here</a> - including improvements to approval (AA) time tracking! You can find more information about the full API changes <a href="https://forum.turkerview.com/threads/view-api-details.2012/" target="_blank" style="text-decoration: underline;">on our announcement here</a>.</p>
3327 <p>Make sure to register & get your new access keys to our upgraded API by <a href="https://turkerview.com/account/api/" target="_blank" style="text-decoration: underline;">visiting your account dashboard</a>. We'll stop displaying this as soon as you do, but the script wont function after February 1st without an API Key.</p>
3328 <form action="saveApiForm" onsubmit="return false;">
3329 <input type="text" class="form-control" style="max-width: 50%; margin-top: 5px; margin-bottom: 5px;">
3330 <button type="submit" class="btn btn-primary">Save API Key</button>
3331 </form>
3332 <script>
3333 $('form[action*=saveApiForm]').submit(function(e){
3334 e.preventDefault();
3335 let api_key = $(this).find('input[type=text]').val().trim();
3336 if (api_key.length == 40){
3337 var store_settings = {
3338 key: 'settings',
3339 api_key: api_key
3340 }
3341
3342 const request = indexedDB.open('turkerview', 1);
3343
3344 request.onsuccess = function(event){
3345 var transaction = event.target.result.transaction(['turkerview'], 'readwrite');
3346 var objectStore = transaction.objectStore('turkerview');
3347 var request2 = objectStore.put(store_settings);
3348
3349 request2.onsuccess = function(event){
3350 let temp_settings = JSON.parse(localStorage.getItem('tv-settings'));
3351 temp_settings.tv_api_key = api_key;
3352 localStorage.setItem('tv-settings', JSON.stringify(temp_settings));
3353 localStorage.setItem('turkerview_api_key', api_key);
3354 alert('Awesome, we saved your API key for future use!');
3355 window.location.reload();
3356 }
3357 };
3358
3359 } else {
3360 alert("We cannot save the provided key as it isn't valid.");
3361 }
3362 });
3363 </script>
3364</div>
3365`;
3366}
3367
3368function tvDashModal(){ return `
3369<div class="modal fade in" id="tvDashModal" style="display: block;">
3370 <div id="tv-dash-dialog" class="modal-dialog">
3371 <div class="modal-content">
3372 <div class="modal-header">
3373 <button id="tv-dash-modal-close" type="button" class="close" data-dismiss="modal"><svg class="close-icon" data-reactid=".8.0.0.0.0.0"><g fill="none" stroke="#555" class="close-icon-graphic" stroke-width="2.117" stroke-linecap="round" data-reactid=".8.0.0.0.0.0.0"><path d="M1.2 1.3l7.4 7.5M1.2 8.8l7.4-7.5" data-reactid=".8.0.0.0.0.0.0.0"></path></g></svg></button>
3374 <h2 class="modal-title">TurkerView Dashboard<span class="pull-right"><a id="tv-dashboard-link" class="hover tv-links" style="padding-right: 5px;" href="#"><i class="fa fa-home"></i></a><a id="tv-settings-link" class="text-muted tv-links" href="#"><i class="fa fa-gear"></i></a></span></h2>
3375 </div>
3376 <div class="modal-body">
3377 <div id="review-alert" class="alert alert-dismissable alert-success hide">
3378 </div>
3379 ${tvApiAlert()}
3380 <div id="tv-dashboard">
3381 <div class="row">
3382 <div class="col-xs-12">
3383 <select class="form-control pull-left" style="width: 25%;" name="tvDateSelection" id="tvDateSelection" >
3384 <option selected value="${today}">Today</option>
3385 <option value="${moment.tz('America/Los_Angeles').subtract(1, "days").format("YYYY-MM-DD")}">Yesterday</option>
3386 <option value="${moment.tz('America/Los_Angeles').subtract(2, "days").format("YYYY-MM-DD")}">${moment().tz('America/Los_Angeles').subtract(2, "days").format("YYYY-MM-DD")}</option>
3387 <option value="${moment.tz('America/Los_Angeles').subtract(3, "days").format("YYYY-MM-DD")}">${moment().tz('America/Los_Angeles').subtract(3, "days").format("YYYY-MM-DD")}</option>
3388 <option value="${moment.tz('America/Los_Angeles').subtract(4, "days").format("YYYY-MM-DD")}">${moment().tz('America/Los_Angeles').subtract(4, "days").format("YYYY-MM-DD")}</option>
3389 </select>
3390 <label class="pull-right text-muted" for="HideReviewed" id="HideReviewedLabel"><input type="checkbox" name="tvHideReviewed" id="HideReviewed" ${hideReviewedFromTable ? 'checked' : ''}> Hide Reviewed</label>
3391 </div>
3392 </div>
3393
3394 <form id="new-zzzreview">
3395 <div class="row">
3396 <div class="col-xs-12 text-muted">
3397 <table id="tv-table" class="mturk-table hits-statuses text-muted">
3398 <thead>
3399 <tr>
3400 <th class="tv-th text-xs-left col-xs-3" style="max-width: 140px;">Requester</th>
3401 <th class="tv-th text-xs-left col-xs-4" style="max-width: 400px;">Title</th>
3402 <th class="tv-th text-xs-right col-xs-1">Hourly</th>
3403 <th class="tv-th text-xs-center col-xs-1"><i class="fa fa-clock-o"></i></th>
3404 <th class="tv-th text-xs-center col-xs-3">Review</th>
3405 </tr>
3406 </thead>
3407 <tbody>
3408
3409 </tbody>
3410 </table>
3411 </div>
3412 </div>
3413 </form>
3414 <p class="text-muted" style="text-align: right"><small>Quick reviews can be enabled in settings (top right)</small></p>
3415 </div>
3416
3417 <div id="tv-settings" style="display: none;">
3418 <div class="row">
3419 <div class="col-xs-12">
3420 <h2 class="primary-color">Main Settings</h2>
3421 <div class="input-group text-muted"><label><input type="checkbox" name="display_titlebar_wage" ${settings.titlebar_wage_display ? `checked` : ``}> Display Hourly Wage in Titlebar</label></div>
3422 <div class="input-group text-muted"><label><input type="checkbox" name="display_mturk_ratings" ${settings.display_mturk_ratings == false ? `` : `checked`}> Display MTurk's System Requester Data</label></div>
3423 <div class="input-group text-muted"><label><input type="checkbox" name="display_requester_ratings" ${settings.display_requester_ratings == false ? `` : `checked`}> Display Requester Ratings</label></div>
3424 <div class="input-group text-muted"><label><input type="checkbox" name="display_hit_ratings" ${settings.display_hit_ratings == false ? `` : `checked`}> Display HIT Ratings</label></div>
3425 <div class="input-group text-muted">
3426 <label><input type="checkbox" name="enable_quick_reviews" ${settings.enable_quick_reviews == true ? `checked` : ``}> Enable Quick Reviews</label>
3427 <small>
3428 <p class="text-warning">Clicking the "Quick Review" button will <strong><u>instantly</u></strong> submit the review!</p>
3429 <p class="text-warning">Quick reviews allows users to one-click submit a time only review (no ratings/writing). If enabled please be careful not to accidentally submit inaccurate data as it can lead to your reviews no longer being accepted.
3430 Some HITs wont be eligible for quick review, but you can always submit a full form review in those cases.</p>
3431 </small>
3432 </div>
3433 <h2>TurkerView API Key:</h2>
3434 <div class="input-group" style="min-width: 65%;"><input type="text" name="tv_api_key" class="form-control search-input" placeholder="API Key" value="${settings.tv_api_key ? settings.tv_api_key : ``}"></div>
3435 <div id="api_connect" style="${(settings.tv_api_key != null && settings.tv_api_key.length == 40 ? `` : `display: none;`)} width: 65%; margin-top: 5px;" class="alert alert-success"><p>Connected!</p></div>
3436 <p class="text-muted"><small>Don't have one? Get your API access key <a href="https://turkerview.com/account/api/" target="_blank" style="text-decoration: underline;">here</a>.</small></p>
3437 </div>
3438 </div>
3439 <div class="row">
3440 <div class="col-xs-12">
3441 <h2 class="primary-color">Return Review Settings</h2>
3442 <div class="input-group text-muted"><label><input type="checkbox" name="exp_returners" ${settings.exp_returners == true ? `checked` : ``}> Experienced Reviewers Only</label> <span class="text-warning"> This will remove a substantial amount of return reviews, usually more helpful to those who have passed the 10,000+ HIT mark</span></div>
3443
3444 <p class="text-muted">
3445 <select name="rr_screener_min" class="form-control" style="width: 15%; display: inline-block;">
3446 <option value="0" ${settings.rr_screener_min <= 0 ? `selected` : ``}>No Filter</option>
3447 <option value="10" ${settings.rr_screener_min == 10 ? `selected` : ``}>10+ seconds</option>
3448 <option value="15" ${settings.rr_screener_min == 15 ? `selected` : ``}>15+ seconds</option>
3449 <option value="30" ${settings.rr_screener_min == 30 ? `selected` : ``}>30+ seconds</option>
3450 <option value="60" ${settings.rr_screener_min == 60 ? `selected` : ``}>1+ minute</option>
3451 <option value="90" ${settings.rr_screener_minunderpaid == 90 ? `selected` : ``}>1½+ minutes</option>
3452 <option value="120" ${settings.rr_screener_min == 120 ? `selected` : ``}>2+ minutes</option>
3453 </select>
3454 <strong>Minimum Unpaid Screener Time</strong> Use this to filter out return reviews for screener levels you don't mind attempting without being paid.
3455 </p>
3456
3457 <!--<label><input type="checkbox" name="show_return_favicon" ${settings.show_return_favicon ? `checked` : ``}> Change Tab Favicon</label>-->
3458 <p style="text-align: center;" class="text-muted"><i class="fa fa-warning text-danger"></i> High | <i class="fa fa-warning text-warning"></i> Medium | <i class="fa fa-warning text-muted"></i> Low</p>
3459 <p class="text-muted">
3460 <select name="return_underpaid_warn_lvl" class="form-control" style="width: 15%; display: inline-block;">
3461 <option value="high" ${settings.return_warning_levels.underpaid == `high` ? `selected` : ``}>High</option>
3462 <option value="medium" ${settings.return_warning_levels.underpaid == `medium` ? `selected` : ``}>Medium</option>
3463 <option value="low" ${settings.return_warning_levels.underpaid == `low` ? `selected` : ``}>Low</option>
3464 <option value="none" ${settings.return_warning_levels.underpaid == `none` ? `selected` : ``}>None</option>
3465 </select>
3466 <strong>Underpaid</strong> Warning Level
3467 </p>
3468 <p class="text-muted">
3469 <select name="return_broken_warn_lvl" class="form-control" style="width: 15%; display: inline-block;">
3470 <option value="high" ${settings.return_warning_levels.broken == `high` ? `selected` : ``}>High</option>
3471 <option value="medium" ${settings.return_warning_levels.broken == `medium` ? `selected` : ``}>Medium</option>
3472 <option value="low" ${settings.return_warning_levels.broken == `low` ? `selected` : ``}>Low</option>
3473 <option value="none" ${settings.return_warning_levels.broken == `none` ? `selected` : ``}>None</option>
3474 </select>
3475 <strong>Broken</strong> Warning Level
3476 </p>
3477 <p class="text-muted">
3478 <select name="return_screener_warn_lvl" class="form-control" style="width: 15%; display: inline-block;">
3479 <option value="high" ${settings.return_warning_levels.screener == `high` ? `selected` : ``}>High</option>
3480 <option value="medium" ${settings.return_warning_levels.screener == `medium` ? `selected` : ``}>Medium</option>
3481 <option value="low" ${settings.return_warning_levels.screener == `low` ? `selected` : ``}>Low</option>
3482 <option value="none" ${settings.return_warning_levels.screener == `none` ? `selected` : ``}>None</option>
3483 </select>
3484 <strong>Unpaid Screener</strong> Warning Level
3485 </p>
3486 <p class="text-muted">
3487 <select name="return_tos_warn_lvl" class="form-control" style="width: 15%; display: inline-block;">
3488 <option value="high" ${settings.return_warning_levels.tos == `high` ? `selected` : ``}>High</option>
3489 <option value="medium" ${settings.return_warning_levels.tos == `medium` ? `selected` : ``}>Medium</option>
3490 <option value="low" ${settings.return_warning_levels.tos == `low` ? `selected` : ``}>Low</option>
3491 <option value="none" ${settings.return_warning_levels.tos == `none` ? `selected` : ``}>None</option>
3492 </select>
3493 <strong>ToS Violation</strong> Warning Level
3494 </p>
3495 <p class="text-muted">
3496 <select name="return_writing_warn_lvl" class="form-control" style="width: 15%; display: inline-block;">
3497 <option value="high" ${settings.return_warning_levels.writing == `high` ? `selected` : ``}>High</option>
3498 <option value="medium" ${settings.return_warning_levels.writing == `medium` ? `selected` : ``}>Medium</option>
3499 <option value="low" ${settings.return_warning_levels.writing == `low` ? `selected` : ``}>Low</option>
3500 <option value="none" ${settings.return_warning_levels.writing == `none` ? `selected` : ``}>None</option>
3501 </select>
3502 <strong>Writing</strong> Warning Level
3503 </p>
3504 <p class="text-muted">
3505 <select name="return_downloads_warn_lvl" class="form-control" style="width: 15%; display: inline-block;">
3506 <option value="high" ${settings.return_warning_levels.downloads == `high` ? `selected` : ``}>High</option>
3507 <option value="medium" ${settings.return_warning_levels.downloads == `medium` ? `selected` : ``}>Medium</option>
3508 <option value="low" ${settings.return_warning_levels.downloads == `low` ? `selected` : ``}>Low</option>
3509 <option value="none" ${settings.return_warning_levels.downloads == `none` ? `selected` : ``}>None</option>
3510 </select>
3511 <strong>Downloads</strong> Warning Level
3512 </p>
3513 <p class="text-muted">
3514 <select name="return_extraordinary_warn_lvl" class="form-control" style="width: 15%; display: inline-block;">
3515 <option value="high" ${settings.return_warning_levels.extraordinary_measures == `high` ? `selected` : ``}>High</option>
3516 <option value="medium" ${settings.return_warning_levels.extraordinary_measures == `medium` ? `selected` : ``}>Medium</option>
3517 <option value="low" ${settings.return_warning_levels.extraordinary_measures == `low` ? `selected` : ``}>Low</option>
3518 <option value="none" ${settings.return_warning_levels.extraordinary_measures == `none` ? `selected` : ``}>None</option>
3519 </select>
3520 <strong>Extraordinary Measures</strong> Warning Level
3521 </p>
3522
3523 </div>
3524 </div>
3525 </div>
3526
3527 </div>
3528 </div>
3529 </div>
3530</div>
3531<div id="tv-dash-modal-backdrop" class="modal-backdrop fade in"></div>`;
3532}
3533
3534function initTurkerView(){
3535 $('footer').after(tvDashModal());
3536
3537 const getCellValue = (tr, idx) => tr.children[idx].innerText.replace('$', '') || tr.children[idx].textContent.replace('$', '');
3538
3539 const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
3540 v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
3541 )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
3542
3543 document.querySelectorAll('th.tv-th').forEach(th => th.addEventListener('click', (() => {
3544 const table = th.closest('table');
3545 const tbody = table.querySelector('tbody');
3546 Array.from(tbody.querySelectorAll('tr'))
3547 .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
3548 .forEach(tr => tbody.appendChild(tr) );
3549 })));
3550
3551 $('#tv-dash-modal-backdrop, #tv-dash-modal-close, #tvDashModal').on('click', function(){
3552 const noticeAlert = document.getElementById('review-alert');
3553 noticeAlert.classList.remove('alert-success');
3554 noticeAlert.classList.remove('alert-warning');
3555 noticeAlert.classList.remove('alert-danger');
3556 noticeAlert.classList.add('hide');
3557 noticeAlert.style.display = 'none';
3558 noticeAlert.innerHTML = '';
3559 $('.dropdown-menu').css('display', 'none');
3560 $('#tv-dash-modal-backdrop, #tvDashModal').hide();
3561 $('body').removeClass('global-modal-open modal-open');
3562 });
3563
3564 $('#tv-dash-dialog').click(function(e){
3565 if (!e.target.classList.contains('btn-quick-review-dropdown') && !e.target.classList.contains('fa-thumbs-up')) $('.dropdown-menu').css('display', 'none');
3566 e.stopPropagation();
3567 });
3568
3569 $('.tv-links').click(function(){
3570 $('.tv-links').removeClass('hover').addClass('text-muted');
3571 $(this).removeClass('text-muted').addClass('hover');
3572 if ($(this).attr('id') == 'tv-dashboard-link' && !$('#tv-dashboard').is(':visible')) $('#tv-dashboard, #tv-settings').toggle();
3573 else if ($(this).attr('id') == 'tv-settings-link' && !$('#tv-settings').is(':visible')) $('#tv-dashboard, #tv-settings').toggle();
3574 });
3575
3576 $(document).on('change', '#tvDateSelection', function(){
3577 fillTable();
3578 });
3579}
3580
3581function tvModal() {
3582 return `
3583<div class="modal fade in" id="tvModal" style="display: none;">
3584 <div id="tv-dialog" class="modal-dialog">
3585 <div class="modal-content">
3586 <div class="modal-header">
3587 <button id="tv-modal-close" type="button" class="close" data-dismiss="modal"><svg class="close-icon" viewBox="0 0 9.9 10.1" data-reactid=".8.0.0.0.0.0"><g fill="none" stroke="#555" class="close-icon-graphic" stroke-width="2.117" stroke-linecap="round" data-reactid=".8.0.0.0.0.0.0"><path d="M1.2 1.3l7.4 7.5M1.2 8.8l7.4-7.5" data-reactid=".8.0.0.0.0.0.0.0"></path></g></svg></button>
3588 <h2 class="modal-title">TurkerView Review</h2>
3589 </div>
3590 <div class="modal-body">
3591 <input type="hidden" name="hitKey" id="hitKey">
3592 <form id="new-review" action="" method="POST">
3593 <div id="review-failure" class="alert alert-dismissable alert-danger hide">
3594 <button type="button" class="close" data-dismiss="alert">×</button>
3595 <h4>Red Alert!</h4>
3596 <p>Why are you looking at this? Curiousity killed the cat batch. We'll add something here if there's a problem! :)</p>
3597 </div>
3598 <div class="row">
3599 <div class="col-xs-7">
3600 <h3>Requester Name</h3>
3601 <input type="text" class="form-control input-group input-group-sm" name="req_name" id="req_name" required placeholder="Requester Name">
3602 </div>
3603 <div class="col-xs-5">
3604 <h3>Requester ID</h3>
3605 <input type="text" class="form-control input-group input-group-sm" name="req_id" id="req_id" required placeholder="Requester ID">
3606 </div>
3607 </div>
3608 <div class="row">
3609 <div class="col-xs-12">
3610 <h3>HIT Title</h3>
3611 <input type="text" class="form-control input-group input-group-sm" name="hit_title" id="hit_title" required placeholder="HIT Title">
3612 <input type="hidden" name="group_id" id="group_id">
3613 </div>
3614 </div>
3615 <div class="row">
3616 <div class="alert alert-dismissable alert-success">
3617 <button type="button" class="close" data-dismiss="alert">×</button>
3618 <h4>Oh would you look at the time!?</h4>
3619 <p>We've pre-filled your completion time for you, but please take a moment to double check that:
3620 <ul class="fa-ul">
3621 <li><i class="fa-li fa fa-check-square-o"></i> The time looks accurate (opening/closing tabs can make it wonky)
3622 <li><i class="fa-li fa fa-check-square-o"></i> The estimated hourly makes sense
3623 <li><i class="fa-li fa fa-check-square-o"></i> You've adjusted for breaks & other factors that would make it unhelpful/inaccurate for most workers (try to help others!)
3624 </ul>
3625 </p>
3626 </div>
3627 <div class="col-xs-6">
3628 <h3>Wage Data</h3>
3629 <div class="form-group">
3630 <div class="col-xs-6">
3631 <input type="number" step="0.01" min="0" class="form-control input-sm hourly_field" name="base_pay" id="base_pay" required placeholder="Reward">
3632 </div>
3633 <div class="col-xs-6">
3634 <input type="number" step="0.01" min="0" class="form-control input-sm hourly_field" name="bonus" id="bonus" placeholder="Bonus">
3635 </div>
3636 </div>
3637 </div>
3638 <div class="col-xs-6">
3639 <h3>Completion Time</h3>
3640 <div class="form-group">
3641 <div class="col-xs-6">
3642 <input type="number" step="1" min="0" class="form-control input-sm hourly_field" name="minutes" id="minutes" placeholder="Minutes">
3643 </div>
3644 <div class="col-xs-6">
3645 <input type="number" step="1" min="0" class="form-control input-sm hourly_field" name="seconds" id="seconds" placeholder="Seconds">
3646 </div>
3647 </div>
3648 </div>
3649 </div>
3650 <div class="row">
3651 <div class="col-xs-12">
3652 <h3>Pay Rating<span class="text-primary pull-right" id="resetPay" style="cursor: pointer;"><i class="fa fa-undo"></i> Reset Rating</span></h3>
3653 <input type="hidden" name="pay_rating" id="pay_rating" value="">
3654 <div class="btn-group btn-group-justified" style="display: table; width: 100%; table-layout: fixed;">
3655 <span style="font-weight: bold; font-size: 15px; display: none; position: absolute; top: -24px; right: 0; width: 100%;" class="text-muted" id="wage_est"></span>
3656 <a role="button" id="underpaid" class="btn btn-danger btn-xs btn-pay" style="font-size: 16px; display: table-cell; float: none;" data-rating="1" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3657 <li><i class='fa-li fa fa-times text-danger'></i>Very low or no pay</li>
3658 <li><i class='fa-li fa fa-times text-danger'></i>Frustrating work experience</li>
3659 <li><i class='fa-li fa fa-times text-danger'></i>Inadequate instructions</li>
3660 </ul>" data-original-title="" title=""><small>Underpaid</small></a>
3661 <a role="button" id="low" class="btn btn-danger btn-xs btn-pay" style="font-size: 16px; display: table-cell; float: none;" data-rating="2" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3662 <li><i class='fa-li fa fa-times text-danger'></i>Below US min-wage ($7.25/hr)</li>
3663 <li><i class='fa-li fa fa-exclamation-triangle text-warning'></i>No redeeming qualities to make up for pay</li>
3664 </ul>" data-original-title="" title=""><small>Low</small></a>
3665 <a role="button" id="fair" class="btn btn-warning btn-xs btn-pay" style="font-size: 16px; display: table-cell; float: none;" data-rating="3" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3666 <li><i class='fa-li fa fa-exclamation-triangle text-warning'></i>Minimum wages for task (consider SE taxes!)</li>
3667 <li><i class='fa-li fa fa-exclamation-triangle text-warning'></i>Work experience offers nothing to tip the scales in a positive or negative direction</li>
3668 </ul>" data-original-title="" title=""><small>Fair</small></a>
3669 <a role="button" id="good" class="btn btn-success btn-xs btn-pay" style="font-size: 16px; display: table-cell; float: none;" data-rating="4" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3670 <li><i class='fa-li fa fa-check text-success'></i>Pay is above minimum wage, or compensates better than average for the level of effort required.</li>
3671 <li><i class='fa-li fa fa-check text-success'></i>The overall work experience makes up for borderline wages</li>
3672 </ul>" data-original-title="" title=""><small>Good</small></a>
3673 <a role="button" id="generous" class="btn btn-success btn-xs btn-pay" style="font-size: 16px; display: table-cell; float: none;" data-rating="5" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3674 <li><i class='fa-li fa fa-check text-success'></i>Pay is exceptional.</li>
3675 <li><i class='fa-li fa fa-check text-success'></i>Interesting, engaging work or work environment</li>
3676 <li><i class='fa-li fa fa-check text-success'></i>Concise instructions, well designed HIT.</li>
3677 </ul>" data-original-title="" title=""><small>Generous</small></a>
3678 </div>
3679 </div>
3680 </div>
3681 <div class="row" id="comm-rating-row">
3682 <div class="col-xs-12">
3683 <h3>Comm Rating<span class="text-primary pull-right" id="resetComm" style="cursor: pointer;"><i class="fa fa-undo"></i> Reset Rating</span></h3>
3684 <input type="hidden" name="comm_rating" id="comm_rating" value="">
3685 <div class="btn-group btn-group-justified" style="display: table; width: 100%; table-layout: fixed;">
3686 <a class="btn btn-danger btn-xs btn-comm" style="font-size: 16px; display: table-cell; float: none;" data-rating="1" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3687 <li><i class='fa-li fa fa-times text-danger'></i>No response at all</li>
3688 <li><i class='fa-li fa fa-times text-danger'></i>Rude response without a resolution</li>
3689 </ul>" data-original-title="" title=""><small>Unacceptable</small></a>
3690 <a class="btn btn-danger btn-xs btn-comm" style="font-size: 16px; display: table-cell; float: none;" data-rating="2" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3691 <li><i class='fa-li fa fa-times text-danger'></i>Responsive, but unhelpful</li>
3692 <li><i class='fa-li fa fa-exclamation-triangle text-warning'></i>Required IRB or extra intervention</li>
3693 </ul>" data-original-title="" title=""><small>Poor</small></a>
3694 <a class="btn btn-warning btn-xs btn-comm" style="font-size: 16px; display: table-cell; float: none;" data-rating="3" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3695 <li><i class='fa-li fa fa-minus text-muted'></i>Responded in a reasonable timeframe</li>
3696 <li><i class='fa-li fa fa-minus text-muted'></i>Resolves issues to a minimum level of satisfaction.</li>
3697 </ul>" data-original-title="" title=""><small>Acceptable</small></a>
3698 <a class="btn btn-success btn-xs btn-comm" style="font-size: 16px; display: table-cell; float: none;" data-rating="4" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3699 <li><i class='fa-li fa fa-check text-success'></i>Prompt Response</li>
3700 <li><i class='fa-li fa fa-check text-success'></i>Positive resolution</li>
3701 </ul>" data-original-title="" title=""><small>Good</small></a>
3702 <a class="btn btn-success btn-xs btn-comm" style="font-size: 16px; display: table-cell; float: none;" data-rating="5" data-toggle="popover" data-title="Suggested Guidelines" data-content="<ul class='fa-ul'>
3703 <li><i class='fa-li fa fa-check text-success'></i>Prompt response time</li>
3704 <li><i class='fa-li fa fa-check text-success'></i>Friendly & Professional</li>
3705 <li><i class='fa-li fa fa-check text-success'></i>Helpful / Solved Issues</li>
3706 <li><i class='fa-li fa fa-check text-success'></i>Interacts within the community</li>
3707 </ul>" data-original-title="" title=""><small>Excellent</small></a>
3708 </div>
3709 </div>
3710 </div>
3711 <div class="row">
3712 <div class="col-xs-12">
3713 <h3>HIT Status</h3>
3714 <div id="batch_alert" class="alert alert-warning hide">
3715 <h3>Head's Up</h3>
3716 <p>Because of the increased importance of rejection information for this HIT we'll update the approval <strong>and rejection</strong> information automatically as it comes in (for this review only). You can also wait until all tasks have approved/rejected to review this HIT.</p>
3717 </div>
3718 <input type="hidden" name="hit_status" id="hit_status" value="0">
3719 <input type="hidden" name="requester_feedback" id="requester_feedback">
3720 <input type="hidden" name="approval_time" id="approval_time">
3721 <div id="hit_status_display">
3722
3723 </div>
3724 </div>
3725 </div>
3726 <div class="row">
3727 <div class="col-xs-12">
3728 <h3 class="text-success">Pros</h3>
3729 <textarea class="form-control input-sm" rows="3" name="review_pros" id="review_pros"></textarea>
3730 <small id="pro-footnote" class="text-muted">What did you like about the HIT or working for this Requester?</small>
3731 </div>
3732 </div>
3733 <div class="row">
3734 <div class="col-xs-12">
3735 <h3 class="text-danger">Cons</h3>
3736 <textarea class="form-control input-sm" rows="3" name="review_cons" id="review_cons"></textarea>
3737 <small id="con-footnote" class="text-muted">What did you dislike about the HIT or working for this Requester?</small>
3738 </div>
3739 </div>
3740 <div class="row">
3741 <div class="col-xs-12">
3742 <h3>Advice to Requester</h3>
3743 <textarea class="form-control input-sm" rows="2" name="review_advice" id="review_advice"></textarea>
3744 <small class="form-text text-muted">Feel free to leave advice/feedback directly for the Requester - but please keep it professional.</small>
3745 </div>
3746 </div>
3747 <div class="row">
3748 <div class="col-xs-12">
3749 <input type="hidden" name="version" value="${ver}">
3750 <input type="hidden" name="app_range">
3751 <input type="hidden" name="app_rate">
3752 <input type="hidden" name="tasks_completed">
3753 <input type="hidden" name="tasks_approved">
3754 <input type="hidden" name="tasks_rejected">
3755 <input type="hidden" name="tracked_completion">
3756 <input type="hidden" name="submit_date">
3757 <label class="text-muted pull-right" >
3758 <button type="button" class="btn btn-success" id="no_share_button" style="padding-right: 15px;" data-toggle="popover" data-title="Don't Share This Review" data-content="<p>Checking this will stop your review from being posted to the current Daily Thread on TurkerHub. Please consider leaving this unchecked as cross-posting helps people see your review & vote it as helpful.</p>
3759 Reviews don't need to be shared if:
3760 <ul class='fa-ul'>
3761 <li><i class='fa-li fa fa-check-circle-o'></i>You're leaving many reviews in a row (avoid SPAM)</li>
3762 <li><i class='fa-li fa fa-check-circle-o'></i>Reviewing old HITs/Requesters that may not be relevant to today's posted HITs</li>
3763 <li><i class='fa-li fa fa-check-circle-o'></i>Quick time reports on well established HITs (C-SATs, etc)</li>
3764 </ul>
3765 Reviews should be shared if:
3766 <ul class='fa-ul'>
3767 <li><i class='fa-li fa fa-check-circle-o'></i>They contain high quality written information about a HIT/Requester</li>
3768 <li><i class='fa-li fa fa-check-circle-o'></i>They report a survey time from a recently posted HIT</li>
3769 <li><i class='fa-li fa fa-check-circle-o'></i>They report rejections, blocks, or other high priority information</li>
3770 </ul>" data-original-title="" title="">Submit Only</button>
3771 <button type="submit" id="btn-review-submit" class="btn btn-primary">Submit & Export</button>
3772 <div style="display: none;"><input type="checkbox" name="no_share" id="no_share"><small> Don't Share</small></div>
3773 <script>
3774 $('#no_share_button').click(function(){
3775 document.getElementById("no_share").checked = true;
3776 $('#btn-review-submit').click();
3777 });
3778 </script>
3779 </label>
3780
3781
3782 </div>
3783 </div>
3784 </form>
3785 </div>
3786 </div>
3787 </div>
3788</div>
3789<div id="tv-modal-backdrop" class="modal-backdrop fade in" style="display: none;"></div>
3790`;
3791}
3792
3793function initReviews(){
3794 $('footer').after(tvModal());
3795 $('body').addClass('global-modal-open modal-open'); //we do this b/c we only add the modal once its called by the user, so need to trigger it as if modal loads on pageload
3796
3797 $('#tv-modal-backdrop, #tv-modal-close, #tvModal').on('click', function(){
3798 //$('#tv-modal-backdrop, #tvModal, #review-failure').hide();
3799 $('#tvDashModal, #tv-dash-modal-backdrop, #tvModal, #tv-modal-backdrop').toggle();
3800 //$('body').removeClass('global-modal-open modal-open');
3801 });
3802
3803 $('#tv-dialog').click(function(e){
3804 e.stopPropagation();
3805 });
3806
3807 let payProOverride = false;
3808 let payConOverride = false;
3809 let commProOverride = false;
3810 let commConOverride = false;
3811
3812 $('#resetPay').on('click', function(){
3813 $('#pay_rating').removeAttr('value');
3814 $('.btn-pay').removeClass('active').children('i').remove();
3815 requireWriting();
3816 });
3817
3818 $('#resetComm').on('click', function(){
3819 $('#comm_rating').removeAttr('value');
3820 $('.btn-comm').removeClass('active').children('i').remove();
3821 requireWriting();
3822 });
3823
3824 function requireWriting(){
3825 let payRating = $('#pay_rating').val();
3826 let commRating = $('#comm_rating').val();
3827 let hourly = $('#wage_est').text().replace(/\/hr/, '').replace(/\$/, '');
3828
3829 //pay stuff
3830 if (parseFloat(hourly) > 10.50 && payRating < 3 && payRating) {
3831 //we're making over $10/hr, why is this low? we need to know the negative experience that warrants the sentiment
3832 payProOverride = false;
3833 payConOverride = true;
3834 $('textarea#review_cons').attr('required', true).attr('minlength', '80');
3835 if ($('#pay_con_req').length == 0) $('#review_cons').before(`
3836<div id="pay_con_req" class="alert alert-danger">
3837 <h3>We Need More Information!</h3>
3838 <p>This field is required when reviewing Pay & Hourly too far apart, please give others details about your experience! Why did you feel the good hourly wages weren\'t worth it?</p>
3839</div>`);
3840 if (commProOverride == false) $('textarea#review_pros').attr('required', false).removeAttr('minlength');
3841 $('#pay_pro_req').remove();
3842 }
3843 else if (parseFloat(hourly) < 7.25 && payRating > 2 && payRating) {
3844 //we're not even making minimum wage on this, why is it fair/good pay? passive work? whats up folks?
3845 payProOverride = true;
3846 payConOverride = false;
3847 $('textarea#review_pros').attr('required', true).attr('minlength', '80');
3848 if ($('#pay_con_req').length == 0) $('#review_pros').before(`
3849<div id="pay_pro_req" class="alert alert-danger">
3850 <h3>We Need More Information!</h3>
3851 <p>This field is required when reviewing Pay & Hourly too far apart, we try to promote fair wages on MTurk so please give others details about your experience! Why did you feel the poor hourly wages were fair for the task?</p>
3852</div>`);
3853 if (commConOverride == false) $('textarea#review_cons').attr('required', false).removeAttr('minlength');
3854 $('#pay_con_req').remove();
3855 } else {
3856 if (commProOverride == false) $('textarea#review_pros').attr('required', false).removeAttr('minlength');
3857 if (commConOverride == false) $('textarea#review_cons').attr('required', false).removeAttr('minlength');
3858 $('#pay_pro_req').remove();
3859 $('#pay_con_req').remove();
3860 }
3861
3862 //comm stuff
3863 if (commRating < 3 && commRating) {
3864 commProOverride = false;
3865 commConOverride = true;
3866 $('textarea#review_cons').attr('required', true).attr('minlength', '80');
3867 if ($('#comm_con_req').length == 0) $('#review_cons').before(`
3868<div id="comm_con_req" class="alert alert-danger">
3869 <h3>We Need More Information!</h3>
3870 <p>This field is required when reviewing Communication, please give others details about your experience! If you did not communicate with the requester please do not rate communication. Its best to give Requesters 2-3 business days before reviewing communication.</p>
3871</div>`);
3872
3873 if (payProOverride == false) $('textarea#review_pros').attr('required', false).removeAttr('minlength');
3874 $('#comm_pro_req').remove();
3875 } else if (commRating >= 3) {
3876 commProOverride = true;
3877 commConOverride = false;
3878 $('textarea#review_pros').attr('required', true).attr('minlength', '80');
3879 if ($('#comm_pro_req').length == 0) $('#review_pros').before(`
3880<div id="comm_pro_req" class="alert alert-danger">
3881 <h3>We Need More Information!</h3>
3882 <p>This field is required when reviewing Communication, please give others details about your experience! If you did not communicate with the requester please do not rate communication.</p>
3883</div>`);
3884 if (payConOverride == false) $('textarea#review_cons').attr('required', false).removeAttr('minlength');
3885 $('#comm_con_req').remove();
3886 } else if (!commRating) {
3887 if (payProOverride == false) $('textarea#review_pros').attr('required', false).removeAttr('minlength');
3888 if (payConOverride == false) $('textarea#review_cons').attr('required', false).removeAttr('minlength');
3889 $('#comm_pro_req').remove();
3890 $('#comm_con_req').remove();
3891 }
3892 }
3893
3894
3895 $('.btn-pay').on('click', function(){
3896 $('.btn-pay').removeClass('active');
3897 $(this).addClass('active');
3898 $('.btn-pay').children('i').remove();
3899 $(this).prepend('<i class="fa fa-check-square-o" style="font-size: .8em;">');
3900 var payRating = $(this).data('rating');
3901 $('input[name=pay_rating]').val(payRating);
3902 requireWriting();
3903 })
3904
3905 location.assign(`javascript:$('.btn-pay, .btn-comm').popover({
3906 html: true,
3907 container: 'body',
3908 trigger: 'hover',
3909 placement: 'top'
3910});
3911$('label, button').popover({
3912 html: true,
3913 container: 'body',
3914 trigger: 'hover',
3915 placement: 'top'
3916 });
3917void(0)`);
3918
3919
3920 $('.btn-comm').on('click', function(){
3921 $('.btn-comm').removeClass('active');
3922 $(this).addClass('active');
3923 $('.btn-comm').children('i').remove();
3924 $(this).prepend('<i class="fa fa-check-square-o" style="font-size: .8em;">');
3925 var commRating = $(this).data('rating');
3926 $('input[name=comm_rating]').val(commRating);
3927 requireWriting();
3928 })
3929
3930 $('.btn-status').on('click', function(){
3931 $('.btn-status').removeClass('active');
3932 $(this).addClass('active');
3933 $('.btn-status').children('i').remove();
3934 $(this).prepend('<i class="fa fa-check-square-o" style="font-size: .8em;">');
3935 })
3936
3937 $('.hourly_field').on('keyup keydown change oninput input', function(){
3938 wageEst();
3939 });
3940
3941 $('#new-review').submit(function(e){
3942 e.preventDefault();
3943
3944 location.assign(`javascript:$('[data-toggle="tooltip"]').tooltip('hide');void(0);`);
3945 $('#btn-review-submit, #no_share_button').attr('disabled', true).prepend('<i class="fa fa-spinner fa-pulse"></i> ');
3946 var url = $(this).attr('action');
3947 var reviewForm = document.getElementById('new-review');
3948 var formData = new FormData(reviewForm);
3949
3950 fetch('https://turkerview.com/api/v2/reviews/submit/', {
3951 method: 'POST',
3952 body: formData,
3953 headers: ViewHeaders
3954 }).then(response => {
3955 if (!response.ok) throw response;
3956
3957 return response.json();
3958 }).then(response => {
3959 //console.log(JSON.stringify(response));
3960 if (response.status == 'ok'){
3961 const noticeAlert = document.getElementById('review-alert');
3962 noticeAlert.classList.remove('alert-success');
3963 noticeAlert.classList.remove('alert-warning');
3964 noticeAlert.classList.remove('alert-danger');
3965 noticeAlert.classList.add('alert-'+response.class);
3966 noticeAlert.classList.remove('hide');
3967 noticeAlert.innerHTML = response.html;
3968
3969 $('form#new-review').trigger('reset');
3970 //$('#req_name, #req_id, #hit_title, #base_pay').attr('readonly', false);
3971 $('.btn-pay, .btn-comm, .btn-status').removeClass('active');
3972 $('.btn-pay, .btn-comm, .btn-status').find('i').remove();
3973 $('#wage_est').text('');
3974 $('#wage_est').hide();
3975 let tempKey = $('input[name=hitKey]').val();
3976 let reviewed = JSON.parse(localStorage.getItem(tempKey));
3977 reviewed['reviewed'] = true;
3978 reviewed['reviewId'] = response.new_review_id;
3979 localStorage.setItem(tempKey, JSON.stringify(reviewed));
3980 $('.btn-review').each(function(){
3981 if (unescape($(this).data('hitkey')) == tempKey) {
3982 $(this).text('Edit').removeClass('btn-primary btn-review').addClass('btn-default');
3983 $(this).attr('href', `https://turkerview.com/reviews/edit.php?id=${response.new_review_id}`).attr('target', '_blank');
3984 }
3985 });
3986 $('input[name=hitKey]').val('');
3987 $('#btn-review-submit, #no_share_button').attr('disabled', false).children('i').remove();
3988 fillTable();
3989 $('#tvDashModal, #tv-dash-modal-backdrop, #tvModal, #tv-modal-backdrop').toggle();
3990 } else{
3991 //console.log(response.postData);
3992 const noticeAlert = document.getElementById('review-failure');
3993 noticeAlert.classList.remove('alert-success');
3994 noticeAlert.classList.remove('alert-warning');
3995 noticeAlert.classList.remove('alert-danger');
3996 noticeAlert.classList.add('alert-'+response.class);
3997 noticeAlert.classList.remove('hide');
3998 noticeAlert.style.display = 'block';
3999 noticeAlert.innerHTML = response.html;
4000
4001 $('#btn-review-submit, #no_share_button').attr('disabled', false).children('i').remove();
4002
4003 $('#tvModal').scrollTop(0)
4004
4005 }
4006
4007 }).catch(exception => {
4008 console.log(exception);
4009 const noticeAlert = document.getElementById('new-review');
4010 noticeAlert.insertAdjacentHTML('beforebegin', `<div class="alert alert-danger"><h3>That's not good!</h3><p>We hit a major error that isn't handled by the server, please send details to CT:</p><p>${exception}</p></div>`);
4011 window.scrollTo(0,0);
4012 $('#tvModal').scrollTop(0)
4013 });
4014
4015 });
4016
4017 fillTable();
4018
4019}
4020
4021function wageEst(){
4022 var basePay = parseFloat($('input[name=base_pay]').val()) || 0;
4023 var bonus = parseFloat($('input[name=bonus]').val()) || 0;
4024
4025 var min = parseFloat($('input[name=minutes]').val()) || 0;
4026 var sec = parseFloat($('input[name=seconds]').val()) || 0;
4027
4028 var totalPay = basePay+bonus;
4029 var totalTime = (min*60)+sec;
4030
4031 var hourly = ( totalPay*(3600/totalTime) ).toFixed(2);
4032
4033 if (hourly !== 'Infinity' && hourly !== 'NaN') {
4034 $('.btn-group.btn-group-justified:contains(Underpaid)').css('padding-top', '16px')
4035 if (hourly < 4.49) { $('#wage_est').removeClass().addClass('text-danger'); var x = $('#wage_est').detach(); $('#underpaid').prepend(x); $('#wage_est').show(); }
4036 else if (hourly > 4.5 && hourly < 7.25) { $('#wage_est').removeClass().addClass('text-danger'); var x = $('#wage_est').detach(); $('#low').prepend(x); $('#wage_est').show(); }
4037 else if (hourly >= 7.25 && hourly < 10.50) { $('#wage_est').removeClass().addClass('text-warning'); var x = $('#wage_est').detach(); $('#fair').prepend(x); $('#wage_est').show(); }
4038 else if (hourly >= 10.50 && hourly < 12.75) { $('#wage_est').removeClass().addClass('text-success'); var x = $('#wage_est').detach(); $('#good').prepend(x); $('#wage_est').show(); }
4039 else if (hourly >= 12.75) { $('#wage_est').removeClass().addClass('text-success'); var x = $('#wage_est').detach(); $('#generous').prepend(x); $('#wage_est').show(); }
4040
4041 $('#wage_est').text('$' + hourly + '/hr');
4042 }
4043}
4044
4045function filterOutliers(someArray) {
4046
4047 if(someArray.length < 4)
4048 return someArray;
4049
4050 let values, q1, q3, iqr, maxValue, minValue;
4051
4052 values = someArray.slice().sort( (a, b) => a - b);//copy array fast and sort
4053
4054 if((values.length / 4) % 1 === 0){//find quartiles
4055 q1 = 1/2 * (values[(values.length / 4)] + values[(values.length / 4) + 1]);
4056 q3 = 1/2 * (values[(values.length * (3 / 4))] + values[(values.length * (3 / 4)) + 1]);
4057 } else {
4058 q1 = values[Math.floor(values.length / 4 + 1)];
4059 q3 = values[Math.ceil(values.length * (3 / 4) + 1)];
4060 }
4061
4062 iqr = q3 - q1;
4063 maxValue = q3 + iqr * 1.5;
4064 minValue = q1 - iqr * 1.5;
4065
4066 return values.filter((x) => (x >= minValue) && (x <= maxValue));
4067}
4068
4069function median(values){
4070 values.sort(function(a,b){
4071 return a-b;
4072 });
4073
4074 if(values.length ===0) return 0
4075
4076 var half = Math.floor(values.length / 2);
4077
4078 if (values.length % 2)
4079 return values[half];
4080 else
4081 return (values[half - 1] + values[half]) / 2.0;
4082}
4083
4084/* CSS */
4085$('head').append(`<style type="text/css">
4086
4087@media (min-width: 768px) {
4088 #tv-dash-dialog {
4089 width: 54rem;
4090 }
4091}
4092
4093@media (min-width: 1224px){
4094 #tv-dash-dialog {
4095 width: 62rem;
4096 }
4097}
4098
4099.turkerview, .turkerviewHIT, .tv-popover {
4100 cursor: default;
4101 display: inline-block;
4102 padding: .25em .4em;
4103 font-size: 85%;
4104 line-height: 1;
4105 text-align: center;
4106 white-space: nowrap;
4107 vertical-align: baseline;
4108 border-radius: 2px;
4109 margin-left: 5px;
4110 margin-right: 5px;
4111 font-weight: 900;
4112}
4113
4114.tv-hide {
4115 display: none;
4116}
4117
4118#hourlyContainer:hover {
4119 opacity: 1 !important;
4120}
4121
4122.tv-popover {
4123color: #555555;
4124font-weight: normal;
4125min-width: 250px;
4126margin-left: 0;
4127margin-top: 22px;
4128position: absolute;
4129z-index: 2000;
4130max-width: 300px;
4131padding: 1px;
4132text-align: left;
4133background-color: #fff;
4134background-clip: padding-box;
4135border: 1px solid rgba(0, 0, 0, 0.2);
4136border-radius: 6px; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
4137white-space: normal;
4138}
4139
4140.tv-popover h3 {
4141margin: 0; padding: 8px 14px; font-size: 15px; font-weight: normal; line-height: 18px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0;
4142}
4143
4144.tv-td {
4145 display: block;
4146 padding: 3px 0;
4147}
4148
4149.loading,
4150.loading::before,
4151.loading::after {
4152 position: absolute;
4153 top: 50%;
4154 left: 50%;
4155 border: 1px solid rgba(24,188,156, 0.2);
4156 border-left-color: rgba(24,188,156, 0.7);
4157 -webkit-border-radius: 999px;
4158 -moz-border-radius: 999px;
4159 border-radius: 999px;
4160}
4161
4162.loading {
4163 margin: -25px 0 0 -25px;
4164 height: 50px;
4165 width: 50px;
4166 -webkit-animation: animation-rotate 2000ms linear infinite;
4167 -moz-animation: animation-rotate 2000ms linear infinite;
4168 -o-animation: animation-rotate 2000ms linear infinite;
4169 animation: animation-rotate 2000ms linear infinite;
4170}
4171
4172.loading::before {
4173 content: "";
4174 margin: -23px 0 0 -23px;
4175 height: 44px;
4176 width: 44px;
4177 -webkit-animation: animation-rotate 2000ms linear infinite;
4178 -moz-animation: animation-rotate 2000ms linear infinite;
4179 -o-animation: animation-rotate 2000ms linear infinite;
4180 animation: animation-rotate 2000ms linear infinite;
4181}
4182
4183.loading::after {
4184 content: "";
4185 margin: -29px 0 0 -29px;
4186 height: 56px;
4187 width: 56px;
4188 -webkit-animation: animation-rotate 4000ms linear infinite;
4189 -moz-animation: animation-rotate 4000ms linear infinite;
4190 -o-animation: animation-rotate 4000ms linear infinite;
4191 animation: animation-rotate 4000ms linear infinite;
4192}
4193
4194@-webkit-keyframes animation-rotate {
4195 100% {
4196 -webkit-transform: rotate(360deg);
4197 }
4198}
4199
4200@-moz-keyframes animation-rotate {
4201 100% {
4202 -moz-transform: rotate(360deg);
4203 }
4204}
4205
4206@-o-keyframes animation-rotate {
4207 100% {
4208 -o-transform: rotate(360deg);
4209 }
4210}
4211
4212@keyframes animation-rotate {
4213 100% {
4214 transform: rotate(360deg);
4215 }
4216}
4217</style>`);
4218
4219$(document).on('mouseover', '.tv-container', function(){
4220 $(this).find('.tv-popover').removeClass('tv-hide');
4221});
4222
4223$(document).on('mouseout', '.tv-container', function(){
4224 $(this).find('.tv-popover').addClass('tv-hide');
4225});
4226
4227$(document).on('mouseover', '.mturk-requester-info', function(){
4228 $(this).closest('li').find('.table-row-popover__content').css('display', 'unset');
4229});
4230
4231$(document).on('mouseout', '.mturk-requester-info', function(){
4232 $(this).closest('li').find('.table-row-popover__content').css('display', 'none');
4233});
4234
4235function slugify(text){
4236 return text.toString().toLowerCase().trim()
4237 .replace(/\s+/g, '-') // Replace spaces with -
4238 .replace(/&/g, '-and-') // Replace & with 'and'
4239 .replace(/[^\w\-]+/g, '') // Remove all non-word chars
4240 .replace(/\-\-+/g, '-'); // Replace multiple - with single -
4241}
4242
4243const tosMap = new Map([
4244 [0, `N/A`],
4245 [1, `<span class="text-muted">Minor Personal Information Violation (Email, Zip, Company Name)</span>`],
4246 [2, `<span class="text-danger">Major Personal Information Violation (Name, Phone #, SSN)</span>`],
4247 [3, `<span class="text-warning">SEO/Referral/Review Fraud</span>`],
4248 [4, `<span class="text-danger">Phishing/Malicious Activity</span>`],
4249 [9, `<span class="text-muted">Misc/Other</span>`]
4250]);
4251const writingMap = new Map([
4252 [0, ``],
4253 [1, `Experiential/Write about a time when...`],
4254 [9, ``]
4255]);
4256const downloadsMap = new Map([
4257 [0, ``],
4258 [1, `Inquisit Software`],
4259 [2, `Browser Extension`],
4260 [3, `Phone/Tablet Apps`],
4261 [9, ``]
4262]);
4263const emMap = new Map([
4264 [0, ``],
4265 [1, `Phone Calls`],
4266 [2, `Webcam / Face requirements`],
4267 [9, ``]
4268]);