User:Schnark/mostEdited.js

/** * User:Schnark/mostEdited.js * * User script to show the pages which were edited most in the last time * For a documentation see User:Schnark/mostEdited * * @author Michael Müller (User:Schnark) * @license GPL (+ CC-BY-SA as all pages in this wiki) * * * Copyright (C) 2011 Michael Müller * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to * * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA  02110-1301, USA. * * */ /*global jQuery: true, mw: true */ ( function ( $ ) {

// since there is no good way to get messages in a user script (yet), jsut put them here

/** * fallback language * Please note the following things: * 1. en is fallback in any way * 2. fallback languages will be resolved only one step * 3. every fallback *must* have an entry in the messages structure * 4. this sucks */ var languageFallback = { 'bar': 'de', 'de-at': 'de', 'de-ch': 'de', 'de-formal': 'de', 'dsb': 'de', 'frr': 'de', 'gsw': 'de', 'hsb': 'de', 'ksh': 'de', 'lb': 'de', 'nds': 'de', 'pdc': 'de', 'pdt': 'de', 'pfl': 'de', 'sli': 'de', 'stq': 'de', 'vmf': 'de' };

var messages = { en: { // from core 'allpagessubmit': 'Go', 'namespace': 'Namespace:', 'invert': 'Invert selection', 'tooltip-invert': 'Check this box to hide changes within the selected namespace (and the associated namespace if checked)', 'namespace_association': 'Associated namespace', 'tooltip-namespace_association': 'Check this box to also include the talk or subject namespace associated with the selected namespace', 'blanknamespace': '(Main)', 'namespacesall': 'all', 'rc-change-size': '$1', 'pagetitle': '$1 - ',

// own messages

'mostedited-legend': 'Most edited pages options', // legend for options on BlankPage with action=mostedited 'mostedited': 'Most edited pages', // link in sidebar and title of the page 'tooltip-n-mostedited': 'Shows the most edited pages', // tooltip for link in sidebar 'mostedited-submit': 'Show most edited pages', // submit button in RecentChanges 'mostedited-time': 'Time:', // label for time selection 'mostedited-edits': '$1 edits ($2 minor edits)', // $1 - total number of edits to the page/section, $2 - number of minor edits 'mostedited-users': '$1 users ($2 anons)', // $1 - total number of different editors, $2 - number of anonymous editors 'mostedited-size': 'Size change: $1', // $1 - formatted number // different times in dropdown 'mostedited-15-minutes': '15 minutes', 'mostedited-30-minutes': '30 minutes', 'mostedited-1-hour': '1 hour', 'mostedited-2-hours': '2 hours', 'mostedited-1-day': '1 day', 'mostedited-other-time': 'other period', 'mostedited-no-pages': 'There are no pages with $1 or more edits in the selected period.', // $1 - number of edits a page must have at least to get shown 'mostedited-increasing': 'The number of edits seems to be increasing.', 'mostedited-unchanging': 'The number of edits seems not to change.', 'mostedited-decreasing': 'The number of edits seems to be decreasing.' },

de: { 'allpagessubmit': 'Anwenden', 'namespace': 'Namensraum:', 'invert': 'Auswahl umkehren', 'tooltip-invert': 'Dieses Auswahlfeld anklicken, um Änderungen im gewählten Namensraum und, sofern ausgewählt, dem entsprechenden zugehörigen Namensraum auszublenden', 'namespace_association': 'Zugeordneter Namensraum', 'tooltip-namespace_association': 'Dieses Auswahlfeld anklicken, um den deiner Auswahl zugehörigen Diskussionsnamensraum, oder im umgekehrten Fall, den zugehörigen Namensraum, mit einzubeziehen', 'blanknamespace': '(Seiten)', 'namespacesall': 'alle', 'rc-change-size': '$1 Bytes', // '$1 NaN Bytess' 'pagetitle': '$1 – ', 'mostedited-legend': 'Anzeigeptionen', 'mostedited': 'Meiste Änderungen', 'tooltip-n-mostedited': 'Zeigt die Seiten mit den meisten Änderungen an', 'mostedited-submit': 'Zeige Seiten mit meisten Änderungen', 'mostedited-time': 'Zeit:', 'mostedited-edits': '$1 Bearbeitungen ($2 kleinere Bearbeitungen)', 'mostedited-users': '$1 Benutzer ($2 anonyme)', 'mostedited-size': 'Größenänderung: $1', 'mostedited-15-minutes': '15 Minuten', 'mostedited-30-minutes': '30 Minuten', 'mostedited-1-hour': '1 Stunde', 'mostedited-2-hours': '2 Stunden', 'mostedited-1-day': '1 Tag', 'mostedited-other-time': 'Andere Dauer', 'mostedited-no-pages': 'Keine Seite wurde im ausgewählten Zeitraum $1 Mal oder häufiger bearbeitet.', 'mostedited-increasing': 'Die Anzahl der Bearbeitungen scheint zuzunehmen.', 'mostedited-unchanging': 'Die Anzahl der Bearbeitungen scheint gleich zu bleiben.', 'mostedited-decreasing': 'Die Anzahl der Bearbeitungen scheint abzunehmen.' },

'de-ch': { 'mostedited-size': 'Grössenänderung: $1' }

};

mw.messages.set( messages.en ); if ( mw.config.get( 'wgUserLanguage' ) in languageFallback ) { mw.messages.set( messages[languageFallback[mw.config.get( 'wgUserLanguage' )]] ); } if ( mw.config.get( 'wgUserLanguage' ) in messages ) { mw.messages.set( messages[mw.config.get( 'wgUserLanguage' )] ); } // allow users to bind to this event to set messages for their language $( document ).trigger( 'mostedited-setmessages' );

/** * replace mw.html.element with a version that accepts boolean attributes * @TODO remove this once MW 1.19 is used */ var oldMwHtmlElement = mw.html.element; mw.html.element = function ( name, attrs, contents ) { var newAttrs = {}; for ( var attrName in attrs ) { var v = attrs[attrName]; // Convert name=true, to name=name if ( v === true ) { v = attrName; // Skip name=false } else if ( v === false ) { continue; }		newAttrs[attrName] = '' + v;	} return oldMwHtmlElement.apply( mw.html, [name, newAttrs, contents] ); };

/** * pagesList contains information for every edited page in the form * 'Pagename': { *	oldsize: 1234, // size of oldest version *	newsize: 4321, // size of newest version *	edits: 123, // number of edits *	minor: 23, // number of minor edits *	users: ['A', 'B'], // all editors *	anons: 5, // number of anonymous editors *	time: 987654321, // sum of all timestamps (in seconds before now) *	sections: { // data for each section *		'Section A': { *			edits: 12, *			minor: 2, *			users: ['A'], *			anons: 2, *			time: 87654321 *		} *	} * } */

var pagesList = {};

/** * current time, time of first edit (milliseconds since 1970-01-01) */ var currTime = 0, firstTime = 0;

/** * converts a timestamp (YYYY-MM-DDTHH:MM:SSZ) into milliseconds since 1970-01-01 * @param timestamp {string} * @return {number} */

function getTime ( timestamp ) { var match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/.exec( timestamp ); return ( new Date( match[1], match[2] - 1, match[3], match[4], match[5], match[6] ) ).getTime; }

/** * This function gets all recent changes in the namespaces starting at the start time. * After the last API call has been done it will call the callback function * @param end {string} end time (timestamp) * @param namespaces {string} namespaces to show, either empty for all or something like '0|1|5' * @param maxcalls {number} maximal number of API calls * @param callback {function} function after the last API call * @param start {string} start time (timestamp), leave empty for first call */ function getAPIRecentChanges ( end, namespaces, maxcalls, callback, start ) { var data = { action: 'query', list: 'recentchanges', rcend: end, rclimit: 'max', rcprop: 'user|comment|title|sizes|flags|timestamp', rctype: 'edit|new', format: 'json' };	if ( start ) { data.rcstart = start; } else { data.meta = 'siteinfo'; data.siprop = 'general'; }	if ( namespaces ) { data.rcnamespace = namespaces; }	$.getJSON( mw.util.wikiScript( 'api' ), data, function ( json ) {		if ( json && json.query && json.query.general ) {			currTime = getTime( json.query.general.time );		}		if ( json && json.query && json.query.recentchanges ) {			var rc = json.query.recentchanges;			for ( var i = 0; i < rc.length; i++ ) {				var edit = rc[i];				if ( !( edit.title in pagesList ) ) {					pagesList[edit.title] = {						newsize: edit.newlen, // the first is the latest edit, so newlen is the most recent size						edits: 0,						minor: 0,						users: [],						anons: 0,						time: 0,						sections: {}					};				}				var section = /^\/\*\s*(.*?)\s*\*\//.exec( edit.comment ); // title of the section				if ( section ) {					section = section[1];				}				if ( section ) {					if ( !( section in pagesList[edit.title].sections ) ) {						pagesList[edit.title].sections[section] = {							edits: 0,							minor: 0,							users: [], anons: 0, time: 0 };					}				}				var time = getTime( edit.timestamp ); firstTime = time; pagesList[edit.title].edits++; // increment edits if ( section ) { pagesList[edit.title].sections[section].edits++; }				if ( edit.minor === '' ) { // increment minor edits pagesList[edit.title].minor++; if ( section ) { pagesList[edit.title].sections[section].minor++; }				}				pagesList[edit.title].oldsize = edit.oldlen; // update oldlen for every edit, only the earliest (= last) is interesting if ( $.inArray( edit.user, pagesList[edit.title].users ) === -1 ) { // store if new user pagesList[edit.title].users.push( edit.user ); if ( edit.anon === '' ) { pagesList[edit.title].anons++; }				}				if ( section ) { if ( $.inArray( edit.user, pagesList[edit.title].sections[section].users ) === -1 ) { pagesList[edit.title].sections[section].users.push( edit.user ); if ( edit.anon === '' ) { pagesList[edit.title].sections[section].anons++; }					}				}				pagesList[edit.title].time += (time - currTime); if ( section ) { pagesList[edit.title].sections[section].time += (time - currTime); }			}		}		if ( maxcalls > 1 && json && json['query-continue'] && json['query-continue'].recentchanges ) { getAPIRecentChanges( end, namespaces, maxcalls - 1, callback, json['query-continue'].recentchanges.rcstart ); } else { callback; }	} ); }

/** * get the pages/sections with the most edits * @param data {object} data about the number of edits, entries must have the form *	'Name': {edits: 123} * @param count {number} number of pages/sections to get * @param edits {number} number of edits needed at least to output a page/section * @return {array} list of the pages/sections with the most edits (decreasing order) */

function getMostEdited ( data, count, edits ) { var items = []; for ( var item in data ) { items.push( item ); }	items.sort( function ( a, b ) {		return data[b].edits - data[a].edits;	} ); var output = items.slice( 0, count ); while ( output.length > 0 && data[output[output.length - 1]].edits < edits ) { output.pop; }	return output; }

/** * encodes a section name to be used as anchor for a link to this section * @param section {string} title of the section * @return {string} encoded anchor */

function encodeSectionLink ( section ) { return mw.util.rawurlencode( section.replace( / /g, '_' ) ) .replace( /%3A/g, ':' ) .replace( /%/g, '.' ) .replace( /^([^a-zA-Z])/, 'x$1' ); }

/** * formats a size change (see ChangesList.php for the original) * @TODO commafy * @param diff {number} difference between old and new size * @return {string} formatted HTML */ function showCharacterDifference ( diff ) { var cssClass, sign = ''; if ( diff < 0 ) { cssClass = 'mw-plusminus-neg'; } else if ( diff > 0 ) { sign = '+'; cssClass = 'mw-plusminus-pos'; } else { cssClass = 'mw-plusminus-null'; }	return mw.html.element( 'span',		{'class': cssClass, dir: 'ltr'},		mw.msg( 'rc-change-size', sign + diff ) ); }

/** * determins whether the edits are increasing or decreasing * @param data {object} object with entries edits and time * @return {string} arrow symbol in a with class and tooltip */

function getTrend ( data ) { var	avgTime = data.time / data.edits, ratio = avgTime / (firstTime - currTime), cssClass, tooltipMsg, arrow; if ( ratio < 0.4 ) { cssClass = 'mw-plusminus-pos'; tooltipMsg = 'mostedited-increasing'; arrow = $( 'body' ).is( '.rtl' ) ? '↖' : '↗';	} else if ( ratio < 0.6 ) { cssClass = 'mw-plusminus-null'; tooltipMsg = 'mostedited-unchanging'; arrow = $( 'body' ).is( '.rtl' ) ? '←' : '→';	} else { cssClass = 'mw-plusminus-neg'; tooltipMsg = 'mostedited-decreasing'; arrow = $( 'body' ).is( '.rtl' ) ? '↙' : '↘';	}	return mw.html.element( 'span', {'class': cssClass, title: mw.msg( tooltipMsg )}, arrow ); }

/** * get HTML for one entry * @param page {string} name of the page to get HTML for * @param count {number} number of sections to show * @param edits {number} number of edits a section must have at least * @return {string} HTML */

function generatePageHTML ( page, count, edits ) { var	data = pagesList[page], html = mw.html.element( 'h2', {}, new mw.html.Raw( mw.html.element( 'a', {href: mw.util.wikiGetlink( page ), title: page}, page ) + getTrend( data ) ) ); html += mw.html.element( 'ul', {}, new mw.html.Raw( mw.html.element( 'li', {}, mw.msg( 'mostedited-edits', data.edits, data.minor ) ) + mw.html.element( 'li', {}, mw.msg( 'mostedited-users', data.users.length, data.anons ) ) + mw.html.element( 'li', {}, new mw.html.Raw( mw.msg( 'mostedited-size', showCharacterDifference( data.newsize - data.oldsize ) ) ) ) ) ); var sections = getMostEdited( data.sections, count, edits ); for ( var i = 0; i < sections.length; i++ ) { var section = sections[i], sectionData = data.sections[section]; html += mw.html.element( 'h3', {}, new mw.html.Raw( mw.html.element( 'a',				{href: mw.util.wikiGetlink( page ) + '#' + encodeSectionLink( section )},				section ) + getTrend( sectionData ) ) ); html += mw.html.element( 'ul', {}, new mw.html.Raw( mw.html.element( 'li', {}, mw.msg( 'mostedited-edits', sectionData.edits, sectionData.minor ) ) + mw.html.element( 'li', {}, mw.msg( 'mostedited-users', sectionData.users.length, sectionData.anons ) ) ) ); }	return html; }

/** * get HTML for complete list * @param countPages {number} number of pages to show * @param countSections {number} number of sections to show for each page * @param editPages {number} number of edits a page must have at least * @param editSections {number} number of edits a section must have at least * @return {string} HTML */

function generateHTML ( countPages, countSections, editPages, editSections ) { var	pages = getMostEdited ( pagesList, countPages, editPages ), html = ''; if ( pages.length === 0 ) { html += mw.msg( 'mostedited-no-pages', editPages ); } else { for ( var i = 0; i < pages.length; i++ ) { html += generatePageHTML( pages[i], countSections, editSections ); }	}	return html; }

/** * get HTML for header * @return {string} HTML */

function generateHeaderHTML { $( '#firstHeading' ).text( mw.msg( 'mostedited' ) ); var	i, html = '', legend = '', labelTime = '', selectTime = '', labelNamespaces = '', selectNamespaces = '', invert = '', associated = '', submit = '', hours, times = [], optionsTime = [], optionsNamespaces = [], formattedNamespaces = mw.config.get( 'wgFormattedNamespaces' ); legend = mw.html.element( 'legend', {}, mw.msg( 'mostedited-legend' ) ); labelTime = mw.html.element( 'label', {'for': 'time'}, mw.msg( 'mostedited-time' ) );

hours = parseInt( mw.util.getParamValue( 'hours' ) || '0', 10 ); if ( hours <= 0 ) { hours = 1; }	times = [ [0.25, 'mostedited-15-minutes'], [0.5, 'mostedited-30-minutes'], [1, 'mostedited-1-hour'], [2, 'mostedited-2-hours'], [24, 'mostedited-1-day'] ];	if ( $.inArray( hours, [0.25, 0.5, 1, 2, 24] ) === -1 ) { times.push( [hours, 'mostedited-other-time'] ); }	for ( i = 0; i < times.length; i++ ) { optionsTime.push( mw.html.element( 'option', {value: times[i][0], selected: times[i][0] === hours}, mw.msg( times[i][1] ) ) ); }	selectTime = mw.html.element( 'select',		{id: 'time', name: 'time', 'class': 'timeselector'},		new mw.html.Raw( optionsTime.join( '' ) ) ); labelNamespaces = mw.html.element( 'label', {'for': 'namespace'}, mw.msg( 'namespace' ) ); optionsNamespaces.push( mw.html.element( 'option', {value: ''}, mw.msg( 'namespacesall' ) ) ); for ( i in formattedNamespaces ) { if ( i < 0 ) { continue; }		var namespace = formattedNamespaces[i]; if ( namespace === '' ) { namespace = mw.msg( 'blanknamespace' ); }		optionsNamespaces.push( mw.html.element( 'option', {value: i, selected: mw.util.getParamValue( 'namespace' ) === i}, // both are strings namespace ) ); }	selectNamespaces = mw.html.element( 'select',		{id: 'namespace', name: 'namespace', 'class': 'namespaceselector'},		new mw.html.Raw( optionsNamespaces.join( '' ) ) ); invert = mw.html.element( 'input',		{name: 'invert', value: 1, id: 'nsinvert', type: 'checkbox', title: mw.msg( 'tooltip-invert' ),			checked: mw.util.getParamValue( 'invert' ) === '1'} ) + ' ' +		mw.html.element( 'label', {'for': 'nsinvert', title: mw.msg( 'tooltip-invert' )}, mw.msg( 'invert' ) ); associated = mw.html.element( 'input',		{name: 'associated', value: 1, id: 'nsassociated', type: 'checkbox', title: mw.msg( 'tooltip-namespace_association' ),			checked: mw.util.getParamValue( 'associated' ) === '1'} ) + ' ' +		mw.html.element( 'label', {'for': 'nsassociated', title: mw.msg( 'tooltip-namespace_association' )}, mw.msg( 'namespace_association' ) ); submit = mw.html.element( 'input', {type: 'button', id: 'submitButton', value: mw.msg( 'allpagessubmit' )} ); html += ' ' + // structure copied from HTML of Special:RecentChanges legend + ' ';	html += mw.html.element( 'div', {id: 'mostEditedContainer'} ); return html; }

/** * called when user clicks submit button */

function submitQuery { var	namespace = $( '#namespace option:selected' ).val, invert = $( '#nsinvert' ).prop( 'checked' ), associated = $( '#nsassociated' ).prop( 'checked' ), namespaces, hours = $( '#time option:selected' ).val, ago = new Date( ( new Date ).getTime - ( hours * 60 * 60 * 1000 ) ), start = String( ago.getUTCFullYear ) + String( ago.getUTCMonth + 101 ).substr( 1 ) + String( ago.getUTCDate + 100 ).substr( 1 ) + String( ago.getUTCHours + 100 ).substr ( 1 ) + String( ago.getUTCMinutes + 100 ).substr( 1 ) + String( ago.getUTCSeconds + 100 ).substr( 1 ), maxCalls = mw.util.getParamValue( 'max-calls' ) || 5, limit = mw.util.getParamValue( 'limit' ) || 10, sectionLimit = mw.util.getParamValue( 'section-limit' ) || 3, edits = mw.util.getParamValue( 'edits' ) || 2, sectionEdits = mw.util.getParamValue( 'section-edits' ) || 2; if ( namespace === '' ) { namespaces = ''; // all } else { namespace = Number( namespace ); var list = [namespace]; if ( associated ) { list.push(				( namespace % 2 === 0 ) ?					namespace + 1 :					namespace - 1 ); }		if ( invert ) { namespaces = []; var formattedNamespaces = mw.config.get( 'wgFormattedNamespaces' ); for ( var i in formattedNamespaces ) { if ( i >= 0 && $.inArray( Number( i ), list ) === -1 ) { namespaces.push( i ); }			}			namespaces = namespaces.join( '|' ); } else { namespaces = list.join( '|' ); }	}	$( '#submitButton' ).prop( 'disabled', true ); // $( '#mostEditedContainer' ).empty.injectSpinner( 'mostedited' ); This needs MW 1.19, so use old wikibits.js	window.injectSpinner( $( '#mostEditedContainer' ).empty.get( 0 ), 'mostedited' ); pagesList = {}; // empty getAPIRecentChanges( start, namespaces, maxCalls, function {		$( '#mostEditedContainer' ).html( generateHTML( limit, sectionLimit, edits, sectionEdits ) );		// $.removeSpinner( 'mostedited' ); This needs MW 1.19, so use old wikibits.js		window.removeSpinner( 'mostedited' );		$( '#submitButton' ).prop( 'disabled', false );	} ); }

/** * initialises the interface on Special:Blankpage, Special:RecentChanges and the sidebar everywhere */

function initBlankpage { document.title = mw.msg( 'pagetitle', mw.msg( 'mostedited' ) ) .replace( /\{\{SITENAME\}\}/, mw.config.get( 'wgSiteName' ) ); // handle mw.util.$content.html( generateHeaderHTML ); // FIXME this clears away subtitle, newtalk and jumpto mw.loader.load( 'mediawiki.special.recentchanges' ); // enables/disables checkboxes $( '#submitButton' ).click( submitQuery ).click; }

function initRecentchanges { var $button = $( mw.html.element( 'input', {type: 'button', value: mw.msg( 'mostedited-submit' )} ) ) .click( function {			var	namespace = $( '#namespace option:selected' ).val,				invert = $( '#nsinvert' ).prop( 'checked' ) ? '1' : '0',				associated = $( '#nsassociated' ).prop( 'checked' ) ? '1' : '0';			document.location.href = mw.util.wikiGetlink( 'Special:BlankPage' ) + '?' +				$.param( {action: 'mostedited', namespace: namespace, invert: invert, associated: associated} );		} ); $( 'input[type="submit"]' ).eq( 0 ).after( $button ); // FIXME breaks when there is another submit button before it }

function initSidebar { var portlet = $( '#n-recentchanges' ).parents( '.portlet, .portal' ).attr( 'id' ) || 'p-navigation'; mw.util.addPortletLink( portlet,		mw.util.wikiGetlink( 'Special:BlankPage' ) + '?action=mostedited',		mw.msg( 'mostedited' ),		'n-mostedited',		mw.msg( 'tooltip-n-mostedited' ),		null, // access key		'#n-recentchanges' ); }

$( initSidebar );

if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Recentchanges' ) { $( initRecentchanges ); }

if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Blankpage' &&	mw.util.getParamValue( 'action' ) === 'mostedited' ) {	/* this needs MW 1.19 mw.loader.using( 'jquery.spinner',		function { */			$( initBlankpage );	//	} ); }

} )( jQuery ); //