[MTurk Worker] HIT Exporter

Allows you to export HITs as formatted text with short, plain, bbcode or markdown styling.

Version au 15/12/2017. Voir la dernière version.

// ==UserScript==
// @name         [MTurk Worker] HIT Exporter
// @namespace    https://github.com/Kadauchi
// @version      1.0.2
// @description  Allows you to export HITs as formatted text with short, plain, bbcode or markdown styling.
// @author       Kadauchi
// @icon         http://i.imgur.com/oGRQwPN.png
// @include      https://worker.mturk.com/*
// @grant        GM_setClipboard
// ==/UserScript==

const hitExports = `all`; // Valid options are: `all`, `short`, `plain`, `bbcode` or `markdown`
const turkerview = true; // Use turkerview in HIT exports
const turkopticon = true; // Use turkopticon in HIT exports
const turkopticon2 = true; // Use turkopticon2 in HIT exports

async function short(event, object) {
    alert(`Short exports are not supported yet`);
}

async function plain(event, object) {
    const hit = object ? object : JSON.parse(event.target.dataset.hit);
    const requesterReview = await getRequesterReview(hit.requester_id);
    const reviewsTemplate = [];

    if (requesterReview.turkerview !== undefined) {
        const tv = requesterReview.turkerview;
        const tvRatings = tv.ratings;

        reviewsTemplate.push([
            `TV:`,
            `[Hrly: $${tvRatings.hourly}]`,
            `[Pay: ${tvRatings.pay}]`,
            `[Fast: ${tvRatings.fast}]`,
            `[Comm: ${tvRatings.comm}]`,
            `[Rej: ${tv.rejections}]`,
            `[ToS: ${tv.tos}]`,
            `[Blk: ${tv.blocks}]`,
            `• https://turkerview.com/requesters/${hit.requester_id}`,

        ].join(` `));

    }
    else if (turkerview === true) {
        reviewsTemplate.push(`TV: No Reviews • https://turkerview.com/requesters/${hit.requester_id}`);
    }

    if (requesterReview.turkopticon !== undefined) {
        const to = requesterReview.turkopticon;
        const toAttrs = to.attrs;

        reviewsTemplate.push([
            `TO:`,
            `[Pay: ${toAttrs.pay}]`,
            `[Fast: ${toAttrs.fast}]`,
            `[Comm: ${toAttrs.comm}]`,
            `[Fair: ${toAttrs.fair}]`,
            `[Reviews: ${to.reviews}]`,
            `[ToS: ${to.tos_flags}]`,
            `• https://turkopticon.ucsd.edu/${hit.requester_id}`,
        ].join(` `));
    }
    else if (turkopticon === true) {
        reviewsTemplate.push(`TO: No Reviews • https://turkopticon.ucsd.edu/${hit.requester_id}`);
    }

    if (requesterReview.turkopticon2 !== undefined) {
        const to2 = requesterReview.turkopticon2;
        const to2Recent = to2.recent;

        reviewsTemplate.push([
            `TO2:`,
            `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
            `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
            `[Res: ${to2Recent.comm[1] > 0 ? `${Math.round(to2Recent.comm[0] / to2Recent.comm[1] * 100)}% of ${to2Recent.comm[1]}` : `---`}]`,
            `[Rec: ${to2Recent.recommend[1] > 0 ? `${Math.round(to2Recent.recommend[0] / to2Recent.recommend[1] * 100)}% of ${to2Recent.recommend[1]}` : `---`}]`,
            `[Rej: ${to2Recent.rejected[0]}]`,
            `[ToS: ${to2Recent.tos[0]}]`,
            `[Brk: ${to2Recent.broken[0]}]`,
            `https://turkopticon.info/requesters/${hit.requester_id}`,
        ].join(` `));
    }
    else if (turkopticon2 === true) {
        reviewsTemplate.push(`TO2: No Reviews • https://turkopticon.info/requesters/${hit.rid}`);
    }

    const exportTemplate = [
        `Title: ${hit.title} • https://worker.mturk.com/projects/${hit.hit_set_id}/tasks • https://worker.mturk.com/projects/${hit.hit_set_id}/tasks/accept_random`,
        `Requester: ${hit.requester_name} • https://worker.mturk.com/requesters${hit.requester_id}/projects`,
        reviewsTemplate.join(`\n`),
        `Reward: ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
        `Duration: ${hit.assignment_duration_in_seconds.toTimeString()}`,
        `Available: ${hit.assignable_hits_count}`,
        `Description: ${hit.description}`,
        `Requirements: ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`,
    ].filter((item) => item !== undefined).join(`\n`);

    GM_setClipboard(exportTemplate);

    const notification = new Notification(`Plain HIT Export has been copied to your clipboard.`);
    setTimeout(notification.close.bind(notification), 10000);
}

async function bbcode(event, object) {
    const hit = object ? object : JSON.parse(event.target.dataset.hit);
    const requesterReview = await getRequesterReview(hit.requester_id);
    const reviewsTemplate = [];

    const ratingColor = (rating) => {
        if (rating > 3.99) {
            return `[color=#00cc00]${rating}[/color]`;
        }
        else if (rating > 2.99) {
            return `[color=#cccc00]${rating}[/color]`;
        }
        else if (rating > 1.99) {
            return `[color=#cc6600]${rating}[/color]`;
        }
        else if (rating > 0.00) {
            return `[color=#cc0000]${rating}[/color]`;
        }
        return rating;
    };

    const percentColor = (rating) => {
        if (rating[1] > 0) {
            const percent = Math.round(rating[0] / rating[1] * 100);

            if (percent > 79) {
                return `[color=#00cc00]${percent}%[/color] of ${rating[1]}`;
            }
            else if (percent > 59) {
                return `[color=#cccc00]${percent}%[/color] of ${rating[1]}`;
            }
            else if (percent > 39) {
                return `[color=#cc6600]${percent}%[/color] of ${rating[1]}`;
            }
            return `[color=#cc0000]${percent}%[/color] of ${rating[1]}`;
        }
        return `---`;
    };

    const goodBadColor = (rating) => {
        return `[color=${rating === 0 ? `#00cc00` : `#cc0000`}]${rating}[/color]`;
    };

    if (requesterReview.turkerview !== undefined) {
        const tv = requesterReview.turkerview;

        reviewsTemplate.push([
            `[b][url=https://turkerview.com/requesters/${hit.requester_id}]TV[/url]:`,
            `[Hrly: $${tv.ratings.hourly}]`,
            `[Pay: ${ratingColor(tv.ratings.pay)}]`,
            `[Fast: ${ratingColor(tv.ratings.fast)}]`,
            `[Comm: ${ratingColor(tv.ratings.comm)}]`,
            `[Rej: ${goodBadColor(tv.rejections)}]`,
            `[ToS: ${goodBadColor(tv.tos)}]`,
            `[Blk: ${goodBadColor(tv.blocks)}][/b]`
        ].join(` `));

    }
    else if (turkerview === true) {
        reviewsTemplate.push(`[b][url=https://turkerview.com/requesters/${hit.requester_id}]TV[/url]:[/b] No Reviews`);
    }

    if (requesterReview.turkopticon !== undefined) {
        const to = requesterReview.turkopticon;
        const toAttrs = to.attrs;

        reviewsTemplate.push([
            `[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:`,
            `[Pay: ${ratingColor(toAttrs.pay)}]`,
            `[Fast: ${ratingColor(toAttrs.fast)}]`,
            `[Comm: ${ratingColor(toAttrs.comm)}]`,
            `[Fair: ${ratingColor(toAttrs.fair)}]`,
            `[Reviews: ${to.reviews}]`,
            `[ToS: ${goodBadColor(to.tos_flags)}][/b]`
        ].join(` `));
    }
    else if (turkopticon === true) {
        reviewsTemplate.push(`[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:[/b] No Reviews`);
    }

    if (requesterReview.turkopticon2 !== undefined) {
        const to2 = requesterReview.turkopticon2;
        const to2Recent = to2.recent;

        reviewsTemplate.push([
            `[b][url=https://turkopticon.info/requesters/${hit.requester_id}]TO2[/url]:`,
            `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
            `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
            `[Res: ${percentColor(to2Recent.comm)}]`,
            `[Rec: ${percentColor(to2Recent.recommend)}]`,
            `[Rej: ${goodBadColor(to2Recent.rejected[0])}]`,
            `[ToS: ${goodBadColor(to2Recent.tos[0])}]`,
            `[Brk: ${goodBadColor(to2Recent.broken[0])}][/b]`
        ].join(` `));
    }
    else if (turkopticon2 === true) {
        reviewsTemplate.push(`[b][url=https://turkopticon.info/requesters/${hit.requester_id}]TO2[/url]:[/b] No Reviews`);
    }

    const exportTemplate = [
        `[b]Title:[/b] [url=https://worker.mturk.com/projects/${hit.hit_set_id}/tasks]${hit.title}[/url] | [url=https://worker.mturk.com/projects/${hit.hit_set_id}/tasks/accept_random]PANDA[/url]`,
        `[b]Requester:[/b] [url=https://worker.mturk.com/requesters/${hit.requester_id}/projects]${hit.requester_name}[/url] [${hit.requester_id}] ([url=https://worker.mturk.com/requesters/${hit.requester_id}]Contact[/url])`,
        reviewsTemplate.join(`\n`),
        `[b]Reward:[/b] ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
        `[b]Duration:[/b] ${hit.assignment_duration_in_seconds.toTimeString()}`,
        `[b]Available:[/b] ${hit.assignable_hits_count}`,
        `[b]Description:[/b] ${hit.description}`,
        `[b]Requirements:[/b] ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`,
    ].filter((item) => item !== undefined).join(`\n`);

    GM_setClipboard(`[table][tr][td]${exportTemplate}[/td][/tr][/table]`);

    const notification = new Notification(`BBCode HIT Export has been copied to your clipboard.`);
    setTimeout(notification.close.bind(notification), 10000);
}

async function markdown(event, object) {
    const hit = object ? object : JSON.parse(event.target.dataset.hit);
    const requesterReview = await getRequesterReview(hit.requester_id);
    const reviewsTemplate = [];

    if (requesterReview.turkerview !== undefined) {
        const tv = requesterReview.turkerview;
        const tvRatings = tv.ratings;

        reviewsTemplate.push([
            `**[TV](https://turkerview.com/requesters/${hit.requester_id}):**`,
            `[Hrly: $${tvRatings.hourly}]`,
            `[Pay: ${tvRatings.pay}]`,
            `[Fast: ${tvRatings.fast}]`,
            `[Comm: ${tvRatings.comm}]`,
            `[Rej: ${tv.rejections}]`,
            `[ToS: ${tv.tos}]`,
            `[Blk: ${tv.blocks}]`
        ].join(` `));

    }
    else if (turkerview === true) {
        reviewsTemplate.push(`TV: No Reviews • https://turkerview.com/requesters/${hit.requester_id}`);
    }

    if (requesterReview.turkopticon !== undefined) {
        const to = requesterReview.turkopticon;
        const toAttrs = to.attrs;

        reviewsTemplate.push([
            `**[TO](https://turkopticon.ucsd.edu/${hit.requester_id}):**`,
            `[Pay: ${toAttrs.pay}]`,
            `[Fast: ${toAttrs.fast}]`,
            `[Comm: ${toAttrs.comm}]`,
            `[Fair: ${toAttrs.fair}]`,
            `[Reviews: ${to.reviews}]`,
            `[ToS: ${to.tos_flags}]`
        ].join(` `));
    }
    else if (turkopticon === true) {
        reviewsTemplate.push(`TO: No Reviews • https://turkopticon.ucsd.edu/${hit.requester_id}`);
    }

    if (requesterReview.turkopticon2 !== undefined) {
        const to2 = requesterReview.turkopticon2;
        const to2Recent = to2.recent;

        reviewsTemplate.push([
            `**[TO2](https://turkopticon.info/requesters/${hit.requester_id}):**`,
            `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
            `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
            `[Res: ${to2Recent.comm[1] > 0 ? `${Math.round(to2Recent.comm[0] / to2Recent.comm[1] * 100)}% of ${to2Recent.comm[1]}` : `---`}]`,
            `[Rec: ${to2Recent.recommend[1] > 0 ? `${Math.round(to2Recent.recommend[0] / to2Recent.recommend[1] * 100)}% of ${to2Recent.recommend[1]}` : `---`}]`,
            `[Rej: ${to2Recent.rejected[0]}]`,
            `[ToS: ${to2Recent.tos[0]}]`,
            `[Brk: ${to2Recent.broken[0]}]`,
            ``,
        ].join(` `));
    }
    else if (turkopticon2 === true) {
        reviewsTemplate.push(`TO2: No Reviews • https://turkopticon.info/requesters/${hit.rid}`);
    }

    const exportTemplate = [
        `> **Title:** [${hit.title}](https://worker.mturk.com/projects/${hit.hit_set_id}/tasks) | [PANDA](https://worker.mturk.com/projects/${hit.hit_set_id}/tasks/accept_random)`,
        `**Requester:** [${hit.requester_name}](https://worker.mturk.com/requesters${hit.requester_id}/projects) [${hit.requester_id}] ([Contact](https://worker.mturk.com/contact?requesterId=${hit.requester_id}))`,
        reviewsTemplate.join(`  \n`),
        `**Reward:** ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
        `**Duration:** ${hit.assignment_duration_in_seconds.toTimeString()}`,
        `**Available:** ${hit.assignable_hits_count}`,
        `**Description:** ${hit.description}`,
        `**Requirements:** ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`,
    ]
    .filter((item) => item !== undefined).join(`  \n`);

    GM_setClipboard(exportTemplate);

    const notification = new Notification(`Markdown HIT Export has been copied to your clipboard.`);
    setTimeout(notification.close.bind(notification), 10000);
}

async function getRequesterReview(id) {
    return new Promise(async (resolve) => {
        const getReview = (stringSite, stringURL) => {
            return new Promise(async (resolve) => {
                try {
                    const response = await fetch(stringURL);

                    if (response.status === 200) {
                        const json = await response.json();
                        resolve([stringSite, json.data ? Object.assign(...json.data.map((item) => ({ [item.id]: item.attributes.aggregates }))) : json]);
                    }
                    else {
                        resolve();
                    }
                }
                catch (error) {
                    resolve();
                }
            });
        };

        const promises = [];

        if (turkerview === true) {
            promises.push(getReview(`turkerview`, `https://turkerview.com/api/v1/requesters/?ids=${id}`));
        }
        if (turkopticon === true) {
            promises.push(getReview(`turkopticon`, `https://turkopticon.ucsd.edu/api/multi-attrs.php?ids=${id}`));
        }
        if (turkopticon2 === true) {
            promises.push(getReview(`turkopticon2`, `https://api.turkopticon.info/requesters?rids=${id}&fields[requesters]=aggregates`));
        }

        const getReviewAll = await Promise.all(promises);

        const objectReview = {};

        for (const item of getReviewAll) {
            if (item && item.length > 0) {
                const site = item[0];
                const reviews = item[1];

                for (const key in reviews) {
                    objectReview[site] = reviews[key];
                }
            }
        }
        resolve(objectReview);
    });
}

(function () {
    const react = document.querySelector(`div[data-react-class="require('reactComponents/hitSetTable/HitSetTable')['default']"]`) ||
          document.querySelector(`div[data-react-class="require('reactComponents/taskQueueTable/TaskQueueTable')['default']"]`);

    if (react) {
        const hitExportButton = (text, callback) => {
            const div = document.createElement(`div`);
            div.className = `col-xs-6`;

            const button = document.createElement(`button`);
            button.className = `btn btn-primary btn-hit-export`;
            button.textContent = text;
            button.style.width = `100%`;
            button.addEventListener(`click`, callback);
            div.appendChild(button);

            return div;
        };

        const modal = document.createElement(`div`);
        modal.className = `modal`;
        modal.id = `hitExportModal`;
        document.body.appendChild(modal);

        const modalDialog = document.createElement(`div`);
        modalDialog.className = `modal-dialog`;
        modal.appendChild(modalDialog);

        const modalContent = document.createElement(`div`);
        modalContent.className = `modal-content`;
        modalDialog.appendChild(modalContent);

        const modalHeader = document.createElement(`div`);
        modalHeader.className = `modal-header`;
        modalContent.appendChild(modalHeader);

        // modal close here

        const modalTitle = document.createElement(`h2`);
        modalTitle.className = `modal-title`;
        modalTitle.textContent = `HIT Export`;
        modalHeader.appendChild(modalTitle);

        const modalBody = document.createElement(`div`);
        modalBody.className = `modal-body`;
        modalContent.appendChild(modalBody);

        const modalBodyRow1 = document.createElement(`div`);
        modalBodyRow1.className = `row`;
        modalBody.appendChild(modalBodyRow1);
        modalBodyRow1.appendChild(hitExportButton(`Short`, short));
        modalBodyRow1.appendChild(hitExportButton(`Plain`, plain));

        const modalBodyRow2 = document.createElement(`div`);
        modalBodyRow2.className = `row`;
        modalBody.appendChild(modalBodyRow2);
        modalBodyRow2.appendChild(hitExportButton(`BBCode`, bbcode));
        modalBodyRow2.appendChild(hitExportButton(`Markdown`, markdown));

        const style = document.createElement(`style`);
        style.innerHTML = `.modal-backdrop.in { z-index: 1049; }`;
        document.head.appendChild(style);

        const json = JSON.parse(react.dataset.reactProps).bodyData;
        const hitRows = react.getElementsByClassName(`table-row`);

        for (let i = 0; i < hitRows.length; i ++) {
            const hit = json[i].project ? json[i].project : json[i];
            const project = hitRows[i].getElementsByClassName(`project-name-column`)[0];

            const button = document.createElement(`button`);
            button.className = `btn btn-primary btn-sm`;
            button.textContent = `Export`;
            button.style.marginRight = `5px`;
            project.prepend(button);

            if (hitExports === `all`) {
                button.dataset.toggle = `modal`;
                button.dataset.target = `#hitExportModal`;
                button.addEventListener(`click`,  (event) => {
                    event.target.closest(`.desktop-row`).click();

                    for (const element of document.getElementsByClassName(`btn-hit-export`)) {
                        element.dataset.hit = JSON.stringify(hit);
                    }
                });
            }
            else {
                button.addEventListener(`click`,  (event) => {
                    event.target.closest(`.desktop-row`).click();

                    if (hitExports === `short`) {
                        short(event, hit);
                    }
                    else if (hitExports === `plain`) {
                        plain(event, hit);
                    }
                    else if (hitExports === `bbcode`) {
                        bbcode(event, hit);
                    }
                    else if (hitExports === `markdown`) {
                        markdown(event, hit);
                    }
                });
            }
        }
    }
})();

Object.assign(Number.prototype, {
    toMoneyString() {
        return `$${this.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
    },
    toTimeString () {
        let day, hour, minute, seconds = this;
        minute = Math.floor(seconds / 60);
        seconds = seconds % 60;
        hour = Math.floor(minute / 60);
        minute = minute % 60;
        day = Math.floor(hour / 24);
        hour = hour % 24;

        let string = ``;

        if (day > 0) {
            string += `${day} day${day > 1 ? `s` : ``} `;
        }
        if (hour > 0) {
            string += `${hour} hour${hour > 1 ? `s` : ``} `;
        }
        if (minute > 0) {
            string += `${minute} day${minute > 1 ? `s` : ``}`;
        }
        return string.trim();
    }
});