MediaWiki:Gadget-QICvote.js

From wikishia

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*
@authors:
* [[User:Wilfredor]]
* [[User:Trougnouf]] (remove the blue backgrounds and fix signature.)
This script make easy the review process on [[Commons:Quality_images_candidates]]
It reduce edit conflicts and you can vote with a simple click (selecting the vote type in the combobox below of each image)
Please, feel free to contact me to improve this tool. Thanks
*
* Section editing is incomplete.  See some issues with the current implementation in T341736#9243771
* Finding the filename in the source in _addVote is, at present, best effort.  See T341736#9247790
*/
mw.loader.using( 'oojs-ui-core' ).done( function () {
$(function() {
    "use strict";
    mw.loader.load( '/w/index.php?title=MediaWiki:Gadget-QICvote.css&action=raw&ctype=text/css', 'text/css' );
    var CONFIRM_BAR =
        "<div class='loading_overly'>"+
        "<div><img src='https://upload.wikimedia.org/wikipedia/commons/c/cd/Vector_Loading_fallback.gif'> Sending</div>"+
        "</div>"+
        "<div class='voting_bar'>" +
        "	<span class='oo-ui-widget oo-ui-widget-enabled oo-ui-inputWidget oo-ui-buttonElement oo-ui-buttonElement-framed oo-ui-labelElement oo-ui-flaggedElement-progressive oo-ui-flaggedElement-primary oo-ui-buttonInputWidget' >" +
        "	  <input type='button' style='margin:auto !important;' tabindex='6' aria-disabled='false'   id='confirm_review' value='Confirm reviews' class='oo-ui-inputWidget-input oo-ui-buttonElement-button'>" +
        "	</span>" +
        "	<div id='loading_'></div>" +
        "	<div id='confirm_'></div>" +
        "	<p style='padding-right:20%;float:right;margin:0'>" +
        "	  <label for='filterNominations'>Show Only</label>" +
        "		<select style='display:inline !important;width:auto;' id='filterNominations' class='submit ui-button ui-widget ui-state-default ui-corner-all ui-button-text-icon-primary ui-button-large'>" +
        "			<option value='Discuss'>Discuss</option>" +
        "			<option value='Promotion' style='color:DarkGreen;' >Promotion</option>" +
        "			<option value='Decline' style='color:DarkRed;' >Decline</option>" +
        "			<option value='Review'>Reviewed</option>" +
        "			<option value='ToReview'>To Review</option>" +
        "			<option value='' selected>All</option>" +
        "		</select>" +
        "	</p>" +
        "</div>";
    var COMBO_REVIEW =
        "<select style='width:100%; margin:0px !important;' class='submit ui-button ui-widget ui-state-default ui-corner-all ui-button-text-icon-primary ui-button-large'>" +
        "   <option value='Promotion' style='color:DarkGreen;'>Promotion</option>" +
        "   <option value='Decline' style='color:DarkRed;'>Decline</option>" +
        "   <option value='Discuss' style='color:DarkGoldenRod;' >Discuss</option>" +
        "   <option value='Nomination'>Comment</option>" +
        "</select>";
    var NOTIFICATION_LAYOUT = '<div class="mw-notification-content">'+
                              '<span class="ui-button-icon-primary ui-icon-green ui-icon ui-icon-check" style="display:inline-block">'+
                              '</span>&nbsp;<span style="font-size: 1em !important;color:green;">Your ';
    var USERNAME = mw.config.get("wgUserName");
    var TITLE = mw.config.get("wgPageName");
    var QIC_PAGE_DESTINY = "Commons:Quality_images_candidates/candidate_list";
    var NOMINATION_CONTAINER = "div.qi";
    var REVIEW_MESSAGE = "Dear " + USERNAME + ", please enter your review comment";

    if (TITLE == "Commons:Quality_images_candidates") {
        TITLE = QIC_PAGE_DESTINY;
    }
    var api = new mw.Api();
    var opt = {
        "Nomination": {
            color: "#0000FF",
            message: "",
            type: "Nomination"
        },
        "Promotion": {
            color: "lime",
            message: "Good quality.",
            type: "Promotion"
        },
        "Decline": {
            color: "red",
            message: "Please fill in a reason why the image does not meet the guidelines.",
            type: "Decline"
        },
        "Comment": {
            color: "#0000FF",
            message: "",
            type: "Comment"
        },
        "Discuss": {
            color: "#EEEE00",
            message: "I disagree.",
            type: "Discuss"
        }
    };

    var votes = {};
    var reviewCount = 0;

    /**
     * Set message of loading.
     *
     * Set the messawe while the reviews 
     * are being sent
     *
     * @param {boolean} show Indicates whether the message should be defined or not
     */
    function _loading(show) {
        if (show) {
            $("#confirm_").text("");
            $("#loading_").text("Sending reviews...");
        } else {
            $("#loading_").text("");
        }
    }
    /**
     * Set confirm messaage.
     *
     * Set the message of review waiting to be sent
     */
    function _confirmMessage() {
        if (reviewCount > 0) {
            $("#confirm_").text(reviewCount + " pending review confirmations");
        }
    }
    /**
     * Get the button background color.
     *
     * Get the button background color from a 
     * button type
     *
     * @param {string} cmbVal button type.
     * @return {string} background color
     */
    function getButtonBg(cmbVal) {
        var buttonBg = null;
        if (cmbVal == "Decline") {
            buttonBg = "red";
        } else {
            if (cmbVal == "Promotion") {
                buttonBg = "green";
            } else {
                if (cmbVal == "Discuss") {
                    buttonBg = "yellow";
                }
            }
        }
        return buttonBg;
    }
    /**
     * Set the combobox class.
     *
     * Set the class for the combobox typeo
     * that define the background color of the combobox
     *
     * @param {object} cmb combobox object.
     * @param {string} className part of the class name to set.
     */
    function setComboClass(cmb) {
        var className = getButtonBg($(cmb).val());
        if (className !== null) {
          $(cmb).addClass("ui-button-" + className);
        } else {
          $(cmb).removeClass("ui-button-green ui-button-red ui-button-yellow");
        }
    }
    /**
     * Get the section html content
     *
     * Get the section conten from the API
     *
     * @param {number} sectionNumber idenfitication of the section.
     * @return {object} section content.
     */
    function getSectionContent(sectionNumber) {
        _loading(true);
        return api.get({
                action: "query",
                prop: "revisions",
                rvprop: ["content", "timestamp"],
                titles: [TITLE],
                //section: sectionNumber, // Future support of section edition
                formatversion: "2",
                curtimestamp: true
            })
            .then(function(data) {
                var page, revision;
                page = data.query.pages[0];
                revision = page.revisions[0];
                return revision.content;
            });
    }

    /**
     * Set new content for a section
     *
     * Update the section content with the new reviews
     *
     * @param {number} sectionNumber idenfitication of the section.
     * @param {object} content new content of the section.
     */
    function editSection(sectionNumber, content) {
        $(".loading_overly").toggle(500);
        $.ajax({
            url: mw.util.wikiScript("api"),
            data: {
                format: "json",
                action: "edit",
                title: TITLE,
                summary: "Reviewing " + reviewCount + " nomination(s) with [[Special:MyLanguage/Help:Gadget-QICvote|QICvote]]",
                text: content,
                //section: sectionNumber, // Future support of section edition
                token: mw.user.tokens.get("csrfToken")
            },
            dataType: "json",
            type: "POST",
            success: function(data) {
                if (data && data.edit && data.edit.result == "Success") {
                    $(".loading_overly").toggle(500);
                    _loading(false);
                    votes = {};
                    $("#confirm_").text("");
                    mw.notify($(NOTIFICATION_LAYOUT + reviewCount + " review(s) have been saved." + "</span></div>"));
                    reviewCount = 0;
                } else if (data && data.error) {
                    mw.notify("Error: API returned error code '" + data.error.code + "' : " + data.error.info);
                } else {
                    mw.notify("Error: Unknown result from API.");
                }
            },
            error: function(xhr) {
                alert("Error: Request failed.");
            }
        });
    }
    /**
     * Get section identification
     *
     * Get section identification from the current nomination
     *
     * @param {object} element a element of nomination on QIC with the class 'gallerybox'.
     * @return {number} id of the section.
     */
    function getSectionId(element) {
        var url = $(element).parent().parent().parent().find("h2 span a:last").attr("href");
        var results = new RegExp("[\?&]section=([^&#]*)").exec(url);
        return (results && results[1]) || 0;
    }

    /**
     * Get image name
     *
     * Get image name of the current nomination
     * using the url of the image how reference
     *
     * @param {object} aElement a element of nomination on QIC with the class 'gallerybox'.
     * @return {string} name of the image.
     */
    function _getImageName(aElement) {
        var imageName = null;
        var imageNode = $(aElement).parent().find("a.image, a.mw-file-description").last().attr("href").split(":");

        if (imageNode.length > 0) {
            if (imageNode[1].indexOf(".") !== -1) {
                imageName = imageNode[1];
            }
        }
        return imageName;
    }

    function _remplaceLast(nomination,vote) {
        var newNomination = getNominationType(nomination, vote.type);
        var newWord = getEOL(newNomination) + getVoteTemplate(vote.type) + vote.comment + _getSignature() + "}}";
        var n = newNomination.lastIndexOf("}}");
        newNomination = newNomination.slice(0, n) + newNomination.slice(n).replace("}}", newWord);
        return newNomination;
    }

    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    function getEOL(newNomination) {
        return (newNomination.slice(-3) === "|}}") ? "" : "<br />";
    }

    function getVoteTemplate(type) {
        var voteTemplate = "";
        if (type == "Promotion") {
            voteTemplate = "{{s}} ";
        } else if (type == "Decline") {
            voteTemplate = "{{o}} ";
        }
        return voteTemplate;
    }

    function _getSignature() {
        // Signature function adapted from MediaWiki:QICSigs.js
        var month = ["January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December"
        ];
        var now = new Date();

        var minute = now.getUTCMinutes();
        if (minute < 10) {
            minute = "0" + minute;
        }

        var hour = now.getUTCHours();
        if (hour < 10) {
            hour = "0" + hour;
        }
        return " --[[User:" + USERNAME + "|" + USERNAME + "]] " + hour + ":" + minute + ", " + now.getUTCDate() + " " +
            month[now.getUTCMonth()] + " " + now.getUTCFullYear() + " (UTC)";
    }

    function _addVote(content, vote) {
        var reg = escapeRegExp(vote.image) + "(.*)\\n";
        var match = content.match(new RegExp(reg, "g"));
        if (!match) {
        	// vote.image is the normalized filename found in the href,
        	// however, it may not be that way in the content.  If we failed
        	// to match, try another variation and hope for the best
        	reg = reg.replace(/_/g, ' ');
        	match = content.match(new RegExp(reg, "g"));
        }
        if (!match) {
        	// Still no match
        	console.log('Failed to find in the content: ', vote.image);
        	return;
        }
        var nomination = match[0];
        var newNomination = _remplaceLast(nomination,vote);
        return content.replace(nomination, newNomination);
    }

    function scapeFilenames(filesname) {
        function removeSpaces(match) {
            return match.replace(/\ /g, "_");
        }
        return filesname.replace(/File:(.*?)\|\{\{+/g, removeSpaces);
    }

    function isDiscuss(oldType, newType) {
        return ((oldType === "Promotion" && newType == "Decline") || (newType === "Promotion" && oldType == "Decline") || oldType == "Discuss");
    }

    function getNominationType(nomination, newType) {
        function setType(oldType) {
            oldType = oldType.replace("{{/", "").replace("|", "");
            if (isDiscuss(oldType, newType)) {
                newType = "Discuss";
            }
            return ("{{/" + newType + "|");
        }
        return nomination.replace(/\{\{\/(Nomination|Promotion|Decline|Discuss)\|/g, setType);
    }

    function _addVotes() {
        $.each(votes, function(sectionNumber, votesSection) {
            if ((typeof votesSection !== "undefined") && (votesSection.length > 0)) {
                getSectionContent(sectionNumber - 1).then(function(content) {
                        content = scapeFilenames(content);
                    $.each(votesSection, function(i, vote) {
                        content = _addVote(content, vote);
                    });
                    editSection(sectionNumber, content);
                });
            }
        });
    }

    function _getNominationContainer(e) {
        return $(e).parent().find(NOMINATION_CONTAINER);
    }

    function _nullVote(cmb) {
        var nullVote = false;
        if ($(cmb).val() === "") {
            _getNominationContainer(cmb).find("ul li i").text("Review needed");
            _getNominationContainer(cmb).css("border-color", "#0000FF");
            $(cmb).val("");
            nullVote = true;
        }
        return nullVote;
    }

    function _getSelection(cmb) {
        var oldType = $(cmb).parent().find("div.qi ul li:nth-child(2)").children(":first").text();
        var newType = $(cmb).val();
        if (isDiscuss(oldType, newType)) {
            newType = "Discuss";
        }
        return opt[newType];
    }

    function _removePreviusVote(cmb) {
        _getNominationContainer(cmb).find("ul li > .coment").remove();
    }

    function _setBoderColorVote(cmb, newColor) {
        _getNominationContainer(cmb).css("border-color", newColor);
    }

    function getOldType(cmb) {
        _getNominationContainer(cmb).find("ul li:nth-child(2)").children(":first").text();
    }

    function _setVoteTitle(cmb, newTitle) {
        _getNominationContainer(cmb).find("ul li:nth-child(2)").children(":first").text(newTitle);
    }

    function _getCommentElement(comment) {
        return $("<li />", {
            text: comment,
            "class": "coment"
        });
    }

    function _getSignatureElement() {
        return $("<a />", {
            href: String("/common/User:" + USERNAME),
            text: USERNAME
        });
    }

    function _getDateElement() {
        return $("<p />", {
            text: String(new Date().toUTCString())
        });
    }

    function _filterNominations(cmb) {
        $(".gallerybox").each(function() {
            $(this).show();
            if ($(cmb).val() !== "") {
                if ($(this).find(NOMINATION_CONTAINER).find("ul:first li:last i").text().trim() !== "Review needed") {
                    if ($(this).find(NOMINATION_CONTAINER).find("ul:first li:last b:first").text() !== $(cmb).val()) {
                        $(this).hide();
                    } else {
                        $(this).show();
                    }
                } else if ($(cmb).val() !== "ToReview") {
                    $(this).hide();
                }
            }
        });
    }

    function _onChangeVote(cmb, section) {
        if (_nullVote(cmb)) {
            return;
        }

        var selection, imageName;
        var comment = "";

        selection = _getSelection(cmb);

        //_removePreviusVote(cmb);
        var textI = {
            placeholder: selection.message
        };
        if (selection.type == "Promotion") {
            textI = {
                value: selection.message
            };
        }

        OO.ui.prompt(REVIEW_MESSAGE, {
            textInput: textI,
            size: 'medium',
            escapable: true,
        }).done(function(result) {

            if (result === null || $.trim(result) === "") {
                $(cmb).val("");
                return;
            }

            _setVoteTitle(cmb, selection.type);
            _setBoderColorVote(cmb, selection.color);
            setComboClass(cmb);

            var $comment = _getCommentElement(result);
            var $signature = _getSignatureElement();
            var $date = _getDateElement();

            $(cmb).parent().find(NOMINATION_CONTAINER).
            find("ul").append($comment).
            append($signature).
            append($date);

            imageName = _getImageName(cmb);

            if (typeof votes[section] === "undefined") {
                votes[section] = [];
            }

            votes[section].push({
                image: decodeURIComponent(imageName),
                comment: result,
                type: $(cmb).val()
            });

            reviewCount++;
            _confirmMessage();
        });
    }

    if (TITLE == QIC_PAGE_DESTINY) {
        var $cmb;
        $("#content").append(CONFIRM_BAR);
        $("#confirm_review").on("click", function() {
            _addVotes();
        });
        $(".gallerybox").each(function() {
            $cmb = $(COMBO_REVIEW).val("");
            $(this).append($cmb);
            var section = getSectionId(this);
            $cmb.on("change", function() {
                _onChangeVote(this, section);
            });
        });

        $("#filterNominations").change(function() {
            _filterNominations(this);
            setComboClass(this);
        });

    }
  });
});