/**
* Page triage stats, top reviewers
* from API action=pagetriagestats
*
* Load from your common.js, for example:
*
* if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Blankpage' ) {
* mw.loader.load( '/w/index.php?title=User:Murph9000/pagetriagestats-topreviewers.js&action=raw&ctype=text/javascript' );
* }
*
* Once added to your common.js, view the dynamically generated special page at:
*
* https://en.wikipedia.org/wiki/Special:BlankPage?action=pagetriagestats&topreviewers=last-day
* https://en.wikipedia.org/wiki/Special:BlankPage?action=pagetriagestats&topreviewers=last-week
* https://en.wikipedia.org/wiki/Special:BlankPage?action=pagetriagestats&topreviewers=last-month
*
* Copyright © 2017 User:Murph9000 @ English Wikipedia. All rights reserved.
*
* Released under the Creative Commons Attribution-ShareAlike 3.0 Unported License
* Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
*
* Released under the Creative Commons Attribution-ShareAlike 4.0 International License
* Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_4.0_International_License
*
* Released under the GNU Free Documentation License
* Wikipedia:Text_of_the_GNU_Free_Documentation_License
*
* Released under the GNU General Public License, version 2 or later
* https://www.gnu.org/licenses/gpl-2.0.html
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
( function ( mw, $ ) {
'use strict';
var FILE = 'User:Murph9000/pagetriagestats-topreviewers.js';
console.info( FILE, 'startup' );
var $content, $spinner, topreviewersParam,
Api,
Html = mw.html,
conf = mw.config.get( [
'wgAction',
'wgCanonicalSpecialPageName',
'wgNamespaceIds',
'wgPageName',
] ),
messages = {
'pagetriagestats-filteredarticle': 'Filtered articles',
'pagetriagestats-filteredarticle-count': 'Count: $1',
'pagetriagestats-reviewedarticle': 'Reviewed articles',
'pagetriagestats-reviewedarticle-count': 'Count: $1',
'pagetriagestats-title': 'Page triage stats',
'pagetriagestats-topreviewers': 'Top reviewers',
'pagetriagestats-topreviewers-caption': 'Top reviewers, $1',
'pagetriagestats-topreviewers-last-day': 'last day',
'pagetriagestats-topreviewers-last-month': 'last month',
'pagetriagestats-topreviewers-last-week': 'last week',
'pagetriagestats-topreviewers-num': 'Number',
'pagetriagestats-topreviewers-total': 'Total',
'pagetriagestats-topreviewers-user': 'User',
'pagetriagestats-unreviewedarticle': 'Unreviewed articles',
'pagetriagestats-unreviewedarticle-count': 'Count: $1',
'pagetriagestats-unreviewedarticle-oldest': 'Oldest: $1',
},
topreviewersValues = [ 'last-day', 'last-week', 'last-month' ],
NS_USER = conf.wgNamespaceIds.user,
NS_USER_TALK = conf.wgNamespaceIds.user_talk,
contributionsTitle = 'Special:Contributions';
/**
* Make user link (or user contributions for unregistered users)
*
* mediawiki-1.28.2/includes/Linker.php
*
* @param int $userId User id in database.
* @param string $userName User name in database.
* @param string $altUserName Text to display instead of the user name (optional)
* @return string HTML fragment
*/
function userLink( userId, userName, altUserName ) {
var page,
classes = 'mw-userlink';
if ( userId === 0 ) {
page = mw.Title.newFromText( contributionsTitle + '/' + userName );
// PHP does a $altUserName = IP::prettifyIP( $userName );
classes += ' mw-anonuserlink'; // Separate link class for anons (bug 43179)
} else {
page = mw.Title.makeTitle( NS_USER, userName );
}
// Wrap the output with <bdi> tags for directionality isolation
return Html.element( 'a', {
href: page.getUrl(),
class: classes,
title: page.getPrefixedText()
}, new Html.Raw(
'<bdi>' +
Html.escape( altUserName !== undefined ? altUserName : userName ) +
'</bdi>'
) );
}
function userToolLinks( userId, userText ) {
var items, page;
items = [];
items.push( userTalkLink( userId, userText ) );
if ( userId ) {
page = mw.Title.newFromText( contributionsTitle + '/' + userText );
items.push( Html.element( 'a', {
href: page.getUrl(),
class: 'mw-usertoollinks-contribs',
title: page.getPrefixedText()
}, mw.msg( 'contribslink' ) )
);
}
return mw.msg( 'word-separator' )
+ '<span class="mw-usertoollinks">'
+ mw.message( 'parentheses',
items.join( mw.msg( 'pipe-separator' ) )
).text()
+ '</span>';
}
/**
* @param int $userId User id in database.
* @param string $userText User name in database.
* @return string HTML fragment with user talk link
*/
function userTalkLink( userId, userText ) {
var page = mw.Title.makeTitle( NS_USER_TALK, userText ),
classes = 'mw-usertoollinks-talk';
return Html.element( 'a', {
href: page.getUrl(),
class: classes,
title: page.getPrefixedText()
}, mw.msg( 'talkpagelinktext' ) );
}
function pagetriagestatsCallback( data ) {
console.log( FILE, 'pagetriagestatsCallback', data );
var stats, topreviewers, rec, total, $div, $table, $tbody;
if ( data.pagetriagestats.result !== 'success' ) {
mw.log.error( FILE, 'API action=pagetriagestats did not return success' );
$content.append(
'<p><big style="color:red">API did not return success</big></p>'
);
return;
}
$div = $( '<div>' ).addClass( 'pagetriagestats' );
$content.append( $div );
stats = data.pagetriagestats.stats;
$div.append(
Html.element( 'dl', { class: 'stats' }, new Html.Raw(
Html.element( 'dt', { class: 'unreviewedarticle' },
mw.msg( 'pagetriagestats-unreviewedarticle' ) ) +
Html.element( 'dd', { class: 'unreviewedarticle-count' },
mw.msg( 'pagetriagestats-unreviewedarticle-count',
stats.unreviewedarticle.count ) ) +
Html.element( 'dd', { class: 'unreviewedarticle-oldest' },
mw.msg( 'pagetriagestats-unreviewedarticle-oldest',
new Date( stats.unreviewedarticle.oldest.replace(
/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/,
'$1-$2-$3T$4:$5:$6Z'
) )
)
) +
Html.element( 'dt', { class: 'reviewedarticle' },
mw.msg( 'pagetriagestats-reviewedarticle' ) ) +
Html.element( 'dd', { class: 'reviewedarticle-count' },
mw.msg( 'pagetriagestats-reviewedarticle-count',
stats.reviewedarticle.reviewed_count ) ) +
Html.element( 'dt', { class: 'filteredarticle' },
mw.msg( 'pagetriagestats-filteredarticle' ) ) +
Html.element( 'dd', { class: 'filteredarticle-count' },
mw.msg( 'pagetriagestats-filteredarticle-count',
stats.filteredarticle ) )
) )
);
topreviewers = stats.topreviewers;
$div.append(
Html.element( 'h2', {}, mw.msg( 'pagetriagestats-topreviewers' ) )
);
$table = $( '<table>' ).addClass( 'topreviewers wikitable' );
$table.append( Html.element( 'caption', {},
mw.msg( 'pagetriagestats-topreviewers-caption',
mw.msg( 'pagetriagestats-topreviewers-' + topreviewersParam ) )
) );
$table.append( Html.element( 'thead', {}, new Html.Raw(
Html.element( 'tr', {}, new Html.Raw(
Html.element( 'th',
{ scope: 'col' },
'#' ) +
Html.element( 'th',
{ scope: 'col', style: 'text-align: left;' },
mw.msg( 'pagetriagestats-topreviewers-user' ) ) +
Html.element( 'th',
{ scope: 'col', style: 'text-align: left;' },
mw.msg( 'pagetriagestats-topreviewers-num' ) )
) )
) ) );
$tbody = $( '<tbody>' );
total = 0;
for ( var i in topreviewers ) {
rec = topreviewers[ i ];
$tbody.append( Html.element( 'tr', {}, new Html.Raw(
Html.element( 'th', { scope: 'row' }, i ) +
Html.element( 'td', {}, new Html.Raw(
userLink( rec.user_id, rec.user_name ) +
userToolLinks( rec.user_id, rec.user_name )
) ) +
Html.element( 'td', {}, rec.num )
) ) );
total += Number(rec.num);
}
$table.append( $tbody );
$table.append( Html.element( 'tfoot', {}, new Html.Raw(
Html.element( 'tr', {}, new Html.Raw(
//Html.element( 'th' ) +
Html.element( 'th',
{ scope: 'row', style: 'text-align: left;', colspan: 2 },
mw.msg( 'pagetriagestats-topreviewers-total' ) ) +
Html.element( 'th',
{ scope: 'col', style: 'text-align: left;' },
total )
) )
) ) );
$div.append( $table );
}
function initPage() {
console.log( FILE, 'ready' );
var title = mw.msg( 'pagetriagestats-title' );
$content = $( '#mw-content-text' );
if ( conf.wgCanonicalSpecialPageName === 'Blankpage' ) {
document.title = mw.msg( 'pagetitle', title );
// Normal skins use #firstHeading
// Mobile skin uses #section_0 for no good reason
$( '#firstHeading, #section_0' ).text( title );
$content.empty();
} else {
$content.append( Html.element( 'h1', {}, title ) );
}
$spinner = $.createSpinner( { size: 'large', type: 'block' } );
$content.append( $spinner );
}
function loaderCallback() {
console.log( FILE, 'loader done' );
var maxage;
if ( conf.wgCanonicalSpecialPageName === 'Blankpage' &&
mw.util.getParamValue( 'action' ) !== 'pagetriagestats' ) {
console.info( FILE + ': rejecting, not a request for this script' );
return $.Deferred().reject();
}
mw.messages.set( messages );
topreviewersParam = mw.util.getParamValue( 'topreviewers' );
if ( !topreviewersParam ||
!topreviewersValues.includes( topreviewersParam ) ) {
topreviewersParam = 'last-week';
}
/**
* https://phabricator.wikimedia.org/diffusion/EPTR/browse/master/includes/PageTriageUtil.php
*
* Data has server-side caching, with expiry as follows:
* last-day: 60 * 60
* last-week: 24 * 60 * 60
* last-month: 24 * 60 * 60
*
* Set our maxage based on that (but a small fraction of it, to avoid
* doubling the delay).
*/
switch ( topreviewersParam ) {
case 'last-day':
maxage = 5 * 60;
break;
//case 'last-week':
//case 'last-month':
default:
maxage = 60 * 60;
}
Api = new mw.Api( {
parameters: {
formatversion: 2
}
} );
return $.when(
Api.get( {
action: 'pagetriagestats',
topreviewers: topreviewersParam,
smaxage: maxage,
maxage: maxage
} ),
$.when(
Api.loadMessagesIfMissing( [
'contribslink',
'pagetitle',
'parentheses',
'pipe-separator',
'talkpagelinktext',
'word-separator',
], {
smaxage: 86400,
maxage: 86400
} ),
$.ready
).then( initPage )
);
}
if ( conf.wgCanonicalSpecialPageName === 'Blankpage' ||
( conf.wgPageName === FILE && conf.wgAction === 'submit' ) ) {
mw.loader.using( [
'mediawiki.Title',
'mediawiki.api',
'mediawiki.api.messages',
'mediawiki.jqueryMsg',
'mediawiki.util',
'jquery.spinner'
] ).then( loaderCallback ).done( function ( data ) {
pagetriagestatsCallback.apply( this, data );
mw.hook( 'wikipage.content' ).fire( $content );
} ).always( function () {
if ( $spinner ) {
$spinner.remove();
}
} );
}
} )( mediaWiki, jQuery );