Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
	REVIEWER SUMMARY
	Description: Shows a summary table of reviewers for the nominations on the current page.
	
	TODO Count all nominators by opening nomination.
*/

if (typeof(ReviewerSummary) == 'undefined') ReviewerSummary = {};
ReviewerSummary.columnSpacePadding = ReviewerSummary.columnSpacePadding || 3;
ReviewerSummary.debug = ReviewerSummary.debug || false;
ReviewerSummary.enableAllPages = ReviewerSummary.enableAllPages || true;
ReviewerSummary.enabledPages = ReviewerSummary.enabledPages ||
[
	'User:Gary/Sandbox',
	
	'Wikipedia:Featured article candidates',
	'Wikipedia:Featured article candidates/Featured log/',
	'Wikipedia:Featured article candidates/Archived nominations/',
	
	'Wikipedia:Featured article review',
	'Wikipedia:Featured article review/archive',
	'Wikipedia:Featured article review/archive/',
	
	'Wikipedia:Featured list candidates',
	'Wikipedia:Featured list candidates/Featured log/',
	'Wikipedia:Featured list candidates/Failed log/',
	
	'Wikipedia:Featured list removal candidates',
	'Wikipedia:Featured list removal candidates/log/',
	
	'Wikipedia:Featured picture candidates',
	'Wikipedia:Featured picture candidates/',
];
ReviewerSummary.useKilobytes = ReviewerSummary.useKilobytes || false;

if (isUnsafe())
{
	var console = unsafeWindow.console;
	mw = unsafeWindow.mw;
}

function addCommas(nStr)
{
	nStr += '';
	var x = nStr.split('.');
	var x1 = x[0];
	var x2 = x.length > 1 ? '.' + x[1] : '';
	var rgx = /(\d+)(\d{3})/;
	while (rgx.test(x1)) 
	{
		x1 = x1.replace(rgx, '$1' + ',' + '$2');
	}
	return x1 + x2;
}

function formatJSON(obj)
{
	if (!obj['query'] || !obj['query']['pages']) return false;
	
	var vars = [];
	vars['pages'] = obj['query']['pages'];
	vars['page'] = [];
	
	for (var i in vars['pages']) vars['page'].push(i);
	if (vars['page'].length != 1) return false;
	
	vars['page'] = obj['query']['pages'][vars['page'][0]];
	vars['pageName'] = vars['page']['title'].replace(/ /g, '_');
	if (vars['page']['revisions']) vars['firstRevision'] = vars['page']['revisions'][0];
	vars['revisions'] = vars['page']['revisions'];
	
	return vars;
}

function isUnsafe()
{
	if (typeof(unsafeWindow) != 'undefined') return true;
	else return false;
}

reviewerSummary = function()
{
	if (mw.util.getParamValue('oldid'))
	{
		alert('Reviewer Summary cannot be used on this page because it is an old revision, so section edit links do not appear.\n\nPlease view the most recent version of this page to use this script.');
		return;
	}
	
	var reviewerTable = $('#reviewer-summary');
	if (reviewerTable.length)
	{
		if (reviewerTable.css('display') != 'none')
		{
			reviewerTable.css('display', 'none');
			return
		}
		else
		{
			reviewerTable.css('display', 'block');
			return;
		}
	}
	
	var reviewerSummary = $('<div id="reviewer-summary"></div>');
	var debug = $('<div id="rs-debug"></div>');
	reviewerSummary.append(debug).append('<span id="reviewer-summary-header" style="font-weight: bold;">Reviewer Summary </span>');
	reviewerSummary.append($('<span id="rs-status"></span>').append('(').append($('<span id="processed-nominations"></span>').append('0')).append(' nominations processed)'));
	var notes = $('<ul><li>To copy the table, highlight the text, and then copy and paste it into an edit box. Wrap the text in <tt>&lt;pre&gt;</tt> tags so that the spacing is retained.<br /><small>Wait until it says "all X nominations processed" above before copying the table.</small></li><li>Click on table headers to sort.</li><li>Numbers in brackets indicate the average per article.</li><li>"<a href="http://en.wikipedia.org/wiki/Byte">Bytes</a>" indicates approximately the total number of characters added/removed.<br /><small>A negative amount here is possible, if the user removed more text than they added, such as in the case of performing maintenance work.</small></li></ul>');
	reviewerSummary.append(notes);
	
	// body
	var body = $('<div id="reviewer-summary-body"></div>');
	var bodyTable = $('<table class="sortable wikitable"><tr id="reviewer-table"><th style="text-align: left;"><a id="sort-username" href="javascript:sortReviewerSummary(\'usernameAsc\');">Username</a><span></span></th><th style="text-align: left;"><a id="sort-articles" href="javascript:sortReviewerSummary(\'articlesAsc\');">Articles</a><span></span></th><th style="text-align: left;"><a id="sort-edits" href="javascript:sortReviewerSummary(\'editsAsc\');">Edits</a><span></span></th><th style="text-align: left;"><a id="sort-bytes" href="javascript:sortReviewerSummary(\'bytesAsc\');">' + (ReviewerSummary.useKilobytes ? 'Kilobytes' : 'Bytes') + '</a><span></span></th></tr></table>'); // (<a href="http://en.wikipedia.org/wiki/Byte">?</a><span></span>)
	reviewerSummary.append(body).append(bodyTable).append('<br />');
	
	var parentNode = $('#bodyContent');
	var allNominations = $('h3', parentNode);
	allNominations.each(function(i)
	{
		var page = $('.editsection', $(this)).eq(0).children().eq(0);
		var pageName = page.attr('title');
		if (!page.length || pageName.indexOf('archive') == -1) return true;
		$.get(mw.config.get('wgScriptPath') + '/api.php', { format: 'json', action: 'query', prop: 'revisions', rvdir: 'newer', rvlimit: 500, rvprop: 'ids|flags|timestamp|user|comment|size', titles: pageName }, reviewerSummaryCallback);
	});

	$('#contentSub').after(reviewerSummary);
}

reviewerSummaryCallback = function(obj)
{
	var vars = formatJSON(obj);
	if (!vars) return;
	var users = {};
	var prevRevision, revision;
	
	for (var i = 0; i < vars['revisions'].length; i++)
	{
		// revision = { 'revid', 'parentid', 'user', 'timestamp', 'comment', 'size' }
		if (vars['revisions'][i - 1]) prevRevision = vars['revisions'][i - 1];
		else prevRevision = false;
		revision = vars['revisions'][i];
		if (!revision['user']) continue;
	
		if (users[revision['user']])
		{
			users[revision['user']]['edits']++;
			users[revision['user']]['size'] += (prevRevision ? revision['size'] - prevRevision['size'] : revision['size']);
		}
		else
		{
			users[revision['user']] = 
			{
				'articles': [],
				'edits': 1,
				'size': (prevRevision ? revision['size'] - prevRevision['size'] : revision['size']),
			};
		}
		
		if (users[revision['user']]['articles'].indexOf(vars['pageName']) == -1) users[revision['user']]['articles'].push(vars['pageName']);
	}
	
	// Do not count the nominator's edits to their own nomination.
	delete(users[vars['firstRevision']['user']]);
	
	var newRow, username, articles, edits, size, usernameText, editsNumber, sizeNumber, editAverage, sizeAverage, editAverageText, sizeAverageText, newArticles, newEdits, newSize, padding;
	for (var user in users)
	{
		var formattedUsername = user.replace(/ /g, '_');
		var oldRow = $('#row-' + formattedUsername);
		var numberOfArticles = users[user]['articles'].length;
		var numberOfEdits = users[user]['edits'];
		var numberOfSize = (ReviewerSummary.useKilobytes ? (users[user]['size'] / 1024).toFixed(0) : users[user]['size']);
		var userArticles = users[user]['articles'].join('; ');
		
		for (var i = ReviewerSummary.enabledPages.length - 1; i > 0; i--)
		{
			var page = ReviewerSummary.enabledPages[i].replace(/ /g, '_');
			
			if (userArticles.indexOf(page) != -1)
			{
				userArticles = userArticles.replace(page, '').replace(/_/g, ' ');
				if (userArticles.substring(0, 1) == '/') userArticles = userArticles.substring(1);
				var split = userArticles.split('/');
				if (split.length > 1) userArticles = split[0];
				break;
			}
		}
		
		if (oldRow.length)
		{
			// Update old values
			articles = $('#articles-' + formattedUsername).children().first().contents().eq(0);
			edits = $('#edits-' + formattedUsername).children().eq(0).contents().eq(0);
			size = $('#size-' + formattedUsername).children().eq(0).contents().eq(0);
			editAverage = $('#edit-average-' + formattedUsername);
			sizeAverage = $('#size-average-' + formattedUsername);
			
			newArticles = parseInt(articles[0].nodeValue) + numberOfArticles;
			newEdits = parseInt(edits[0].nodeValue) + numberOfEdits;
			newSize = parseInt(size[0].nodeValue) + (ReviewerSummary.useKilobytes ? (users[user]['size'] / 1024).toFixed(0) : users[user]['size']);
			articles.parent().attr('title', articles.parent().attr('title') + '; ' + userArticles);
			articles[0].nodeValue = addCommas(newArticles);
			edits[0].nodeValue = addCommas(newEdits);
			size[0].nodeValue = addCommas(newSize);
			
			if (editAverage.contents().length)
			{
				// Row was already updated at least once before
				editAverageText = $('<span title="' + newEdits + ' / ' + newArticles + '"> (' + addCommas((newEdits / newArticles).toFixed(1)) + ')</span>');
				editAverage.contents().eq(0).replaceWith(editAverageText);
				
				sizeAverageText = $('<span title="' + (newSize + ' / ' + newArticles) + '"> (' + addCommas((newSize / newArticles).toFixed(1)) + ')</span>');
				sizeAverage.contents().eq(0).replaceWith(sizeAverageText);
			}
			else
			{
				// Row is being updated for the first time
				editAverage.append(' (' + addCommas((newEdits / newArticles).toFixed(1)) + ')');
				sizeAverage.append(' (' + addCommas((newSize / newArticles).toFixed(1)) + ')');
			}
		}
		else
		{
			// Create a new row
			padding = $('<span></span>');
			username = $('<td id="username-' + formattedUsername + '"></td>').append('<a href="/wiki/User:' + formattedUsername + '">' + user + '</a>').append(padding.clone());
			articles = $('<td id="articles-' + formattedUsername + '"><abbr title="' + userArticles + '">' + numberOfArticles + '</abbr></td>').append(padding.clone());
			edits = $('<td id="edits-' + formattedUsername + '"></td>').append('<span>' + numberOfEdits + '</span>').append('<span id="edit-average-' + formattedUsername + '"></span>').append(padding.clone());
			size = $('<td id="size-' + formattedUsername + '"></td>').append('<span>' + addCommas(numberOfSize) + '</span>').append('<span id="size-average-' + formattedUsername + '"></span>').append(padding.clone());
			$('#reviewer-table').parent().append($('<tr id="row-' + formattedUsername + '"></tr>').append(username).append(articles).append(edits).append(size));
		}
	}
	
	// Pad the rows
	// padReviewerSummaryRows();
	
	// Update number of nominations processed
	var allNominations = $('h3');
	var status = $('#rs-status');
	var processedNominations = $('#processed-nominations').contents().eq(0);
	processedNominations[0].nodeValue = parseInt(processedNominations[0].nodeValue) + 1;
	
	// done processing nominations
	if (allNominations.length == processedNominations[0].nodeValue)
	{
		processedNominations[0].nodeValue = 'all ' + processedNominations[0].nodeValue;
		
		// Sort table
		sortReviewerSummary('usernameAsc');
	}
}

function padReviewerSummaryRows()
{
	var reviewerTable = $('#reviewer-table').parent();
	var rows = reviewerTable.children();
	var usernameNode, articleNode, editNode, sizeNode;	
	
	// Calculate "longest" variables
	var longestUsername = 'Username'.length;
	var longestArticle = 'Articles'.length;
	var longestEdit = 'Edits'.length;
	var longestSize = (ReviewerSummary.useKilobytes ? 'Kilobytes' : 'Bytes').length;
	var replacePattern = /(<([^>]+)>)/gi;	
	
	for (var i = 1; i < rows.length; i++)
	{
		var username = rows[i].childNodes[0].innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, '');
		var article = rows[i].childNodes[1].innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, '');
		var edit = rows[i].childNodes[2].innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, '');
		var size = rows[i].childNodes[3].innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, '');
		
		if (username.length > longestUsername) longestUsername = username.length;
		if (article.length > longestArticle) longestArticle = article.length;
		if (edit.length > longestEdit) longestEdit = edit.length;
		if (size.length > longestSize) longestSize = size.length;
	}
	
	var padding = ReviewerSummary.columnSpacePadding;
	longestUsername += padding;
	longestArticle += padding;
	longestEdit += padding;
	longestSize += padding;
	
	// Pad the header (first) row as well
	firstHeader = rows[0].childNodes[0];
	secondHeader = rows[0].childNodes[1];
	thirdHeader = rows[0].childNodes[2];
	fourthHeader = rows[0].childNodes[3];
	
	var firstPadding = padReviewerSummary(firstHeader.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestUsername, true);
	var secondPadding = padReviewerSummary(secondHeader.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestArticle, true);
	var thirdPadding = padReviewerSummary(thirdHeader.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestEdit, true);
	var fourthPadding = padReviewerSummary(fourthHeader.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestSize, true);
	
	if (firstHeader.lastChild.firstChild)
	{
		if (firstHeader.lastChild.firstChild.nodeValue.length < firstPadding.length)
			firstHeader.lastChild.firstChild.nodeValue = firstPadding;
	}
	else
		firstHeader.lastChild.appendChild(document.createTextNode(firstPadding));
	
	if (secondHeader.lastChild.firstChild)
	{
		if (secondHeader.lastChild.firstChild.nodeValue.length < secondPadding.length)
			secondHeader.lastChild.firstChild.nodeValue = secondPadding;
	}
	else
		secondHeader.lastChild.appendChild(document.createTextNode(secondPadding));
	
	if (thirdHeader.lastChild.firstChild)
	{
		if (thirdHeader.lastChild.firstChild.nodeValue.length < thirdPadding.length)
			thirdHeader.lastChild.firstChild.nodeValue = thirdPadding;
	}		
	else
		thirdHeader.lastChild.appendChild(document.createTextNode(thirdPadding));
				
	if (fourthHeader.lastChild.firstChild)
	{
		if (fourthHeader.lastChild.firstChild.nodeValue.length < fourthPadding.length)
			fourthHeader.lastChild.firstChild.nodeValue = fourthPadding;
	}		
	else
		fourthHeader.lastChild.appendChild(document.createTextNode(fourthPadding));
	
	// Pad the remaining rows
	for (var i = 1; i < rows.length; i++)
	{
		usernameNode = rows[i].childNodes[0];
		articleNode = rows[i].childNodes[1];
		editNode = rows[i].childNodes[2];
		sizeNode = rows[i].childNodes[3];
		
		username = padReviewerSummary(usernameNode.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestUsername, true);
		article = padReviewerSummary(articleNode.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestArticle, true);
		edit = padReviewerSummary(editNode.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestEdit, true);
		size = padReviewerSummary(sizeNode.innerHTML.replace(replacePattern, '').replace(/&nbsp;/g, ''), longestSize, true);
		
		if (usernameNode.lastChild.firstChild.nodeValue.length < username.length) usernameNode.lastChild.firstChild.nodeValue = username;
		if (articleNode.lastChild.firstChild.nodeValue.length < article.length) articleNode.lastChild.firstChild.nodeValue = article;
		if (editNode.lastChild.firstChild.nodeValue.length < edit.length) editNode.lastChild.firstChild.nodeValue = edit;
		if (sizeNode.lastChild.firstChild.nodeValue.length < size.length) sizeNode.lastChild.firstChild.nodeValue = size;
	}
}

function padReviewerSummary(string, length, paddingOnly)
{
	if (string.length >= length)
	{
		if (paddingOnly) return '';
		else return string;
	}
	
	var padding = '';
	while (string.length < length)
	{
		string = string + '\u00a0';
		padding += '\u00a0';
	}
	
	if (paddingOnly) return padding;
	else return string;
}

function reviewerSummaryPortletLink()
{
	var enabledPages = ReviewerSummary.enabledPages
	if (!mw.config.get('wgPageName') || !$('#bodyContent').length) return false;
	var pageIsEnabled = false;
	
	for (var i = 0; i < enabledPages.length; i++)
	{
		if (((mw.config.get('wgPageName').indexOf(enabledPages[i].replace(/ /g, '_')) == 0) && (enabledPages[i].substr(enabledPages[i].length - 1, 1) == '/')) || (mw.config.get('wgPageName') == enabledPages[i].replace(/ /g, '_')))
		{
			pageIsEnabled = true;
			break;
		}
	}
	
	if (pageIsEnabled && (mw.config.get('wgAction') == 'view' || mw.config.get('wgAction') == 'purge') && !mw.util.getParamValue('oldid'))
	{
		if (ReviewerSummary.debug) reviewerSummary();
		createReviewerSummaryPortletLink();
	}
	else if (ReviewerSummary.enableAllPages)
		createReviewerSummaryPortletLink();
}

function createReviewerSummaryPortletLink()
{
	// allowed namespaces
	var allowedNamespaces = ['User', 'User_talk', 'Project', 'Project_talk'];	
	if ($.inArray(mw.config.get('wgCanonicalNamespace'), allowedNamespaces) != -1) return mw.util.addPortletLink('p-tb', 'javascript:reviewerSummary();', 'Reviewer summary', 't-reviewer-summary', 'Show a summary of reviewers');
}

sortReviewerSummary = function(type)
{
	// type = username, articles, edits, bytes
	var reviewerTable = $('#reviewer-table').parent();
	var rows = reviewerTable.children();
	var items = [];
	
	rows.each(function(i)
	{
		if (i == 0) return true;
		
		items.push(
		{
			username: $(this).children().eq(0).text(),
			articles: $(this).children().eq(1).text().replace(/,/g, ''),
			edits:    $(this).children().eq(2).text().replace(/,/g, ''),
			bytes:    $(this).children().eq(3).text().replace(/,/g, ''),
			row:      $(this).clone()
		});
	});
	
	var username = $('#sort-username');
	var articles = $('#sort-articles');
	var edits = $('#sort-edits');
	var bytes = $('#sort-bytes');
	
	switch (type)
	{
		case 'usernameAsc':
			username.attr('href', "javascript:sortReviewerSummary('usernameDesc');");
			items.sort(sortUsername);
			break;
		case 'articlesAsc':
			articles.attr('href', "javascript:sortReviewerSummary('articlesDesc');");
			items.sort(sortArticles);
			break;
		case 'editsAsc':
			edits.attr('href', "javascript:sortReviewerSummary('editsDesc');");
			items.sort(sortEdits);
			break;
		case 'bytesAsc':
			bytes.attr('href', "javascript:sortReviewerSummary('bytesDesc');");
			items.sort(sortBytes);
			break;
			
		case 'usernameDesc':
			username.attr('href', "javascript:sortReviewerSummary('usernameAsc');");
			items.sort(reverseSortUsername);
			break;
		case 'articlesDesc':
			articles.attr('href', "javascript:sortReviewerSummary('articlesAsc');");
			items.sort(reverseSortArticles);
			break;
		case 'editsDesc':
			edits.attr('href', "javascript:sortReviewerSummary('editsAsc');");
			items.sort(reverseSortEdits);
			break;
		case 'bytesDesc':
			bytes.attr('href', "javascript:sortReviewerSummary('bytesAsc');");
			items.sort(reverseSortBytes);
			break;
	}
	
	// remove old rows, add new ones
	for (var i = rows.length - 1; i > 0; i--) rows.eq(i).remove();
	for (var i = 0; i < items.length; i++) reviewerTable.append(items[i]['row']);
}

function reverseSortUsername(a, b)
{
	return sortUsername(b, a);
}

function reverseSortArticles(a, b)
{
	return sortArticles(b, a);
}

function reverseSortEdits(a, b)
{
	return sortEdits(b, a);
}

function reverseSortBytes(a, b)
{
	return sortBytes(b, a);
}

function sortUsername(a, b)
{
	if (a['username'] < b['username']) return -1;
	else return 1;
}

function sortArticles(a, b)
{
	var aArticles = parseInt(a['articles']);
	var bArticles = parseInt(b['articles']);
	
	if (aArticles < bArticles) return -1;
	else if (aArticles > bArticles) return 1;
	else
	{
		if (a['username'] < b['username']) return -1;
		else return 1;
	}
}

function sortEdits(a, b)
{
	var aEdits = parseInt(a['edits']);
	var bEdits = parseInt(b['edits']);
	
	if (aEdits < bEdits) return -1;
	else if (aEdits > bEdits) return 1;
	else
	{
		if (a['username'] < b['username']) return -1;
		else return 1;
	}
}

function sortBytes(a, b)
{
	var aBytes = parseInt(a['bytes']);
	var bBytes = parseInt(b['bytes']);
	
	if (aBytes < bBytes) return -1;
	else if (aBytes > bBytes) return 1;
	else
	{
		if (a['username'] < b['username']) return -1;
		else return 1;
	}
}

if (isUnsafe())
{
	unsafeWindow.reviewerSummary = reviewerSummary;
	unsafeWindow.reviewerSummaryCallback = reviewerSummaryCallback;
	unsafeWindow.sortReviewerSummary = sortReviewerSummary;
}

$.when( mw.loader.using( [ 'mediawiki.util' ] ), $.ready ).done(reviewerSummaryPortletLink);