MediaWiki:Gadget-PrendiInCaricoRichiestaVAR.js

Questa pagina definisce alcuni parametri di aspetto e comportamento generale di tutte le pagine. Per personalizzarli vedi Aiuto:Stile utente.


Nota: dopo aver salvato è necessario pulire la cache del proprio browser per vedere i cambiamenti (per le pagine globali è comunque necessario attendere qualche minuto). Per Mozilla / Firefox / Safari: fare clic su Ricarica tenendo premuto il tasto delle maiuscole, oppure premere Ctrl-F5 o Ctrl-R (Command-R su Mac); per Chrome: premere Ctrl-Shift-R (Command-Shift-R su un Mac); per Konqueror: premere il pulsante Ricarica o il tasto F5; per Opera può essere necessario svuotare completamente la cache dal menù Strumenti → Preferenze; per Internet Explorer: mantenere premuto il tasto Ctrl mentre si preme il pulsante Aggiorna o premere Ctrl-F5.

/**
 * Questo accessorio automatizza tutte le operazioni necessarie per prendere
 * formalmente in carico una richiesta indirizzata alla Commissione arbitrale.
 * 
 * Quando l'utente clicca il pulsante "Prendi in carico" del template:RichiestaVAR,
 * visualizza una richiesta di conferma e poi riceve riscontro delle operazioni
 * in corso in una finestra di dialogo.
 * 
 * @author https://it.wikipedia.org/wiki/Utente:Sakretsu
 */
/* global mediaWiki, jQuery, OO */

( function ( mw, $ ) {
	'use strict';

	const conf = mw.config.get( [
		'wgArticleId',
		'wgNamespaceIds',
		'wgNamespaceNumber',
		'wgPageName',
		'wgRelevantUserName',
		'wgRevisionId',
		'wgUserGroups',
		'wgUserId',
		'wgUserName'
	] );
	const dependencies = [
		'ext.gadget.ProgressDialog',
		'mediawiki.api',
		'mediawiki.util',
		'oojs-ui-core',
		'oojs-ui-widgets',
		'oojs-ui-windows'
	];
	const resolutionRootPage = isOwnSandbox()
		? `Utente:${ conf.wgUserName }/Sandbox/Test richiesta arbcom`
		: 'Wikipedia:Commissione arbitrale/Deliberazioni';
	const resolutionTemplate = isOwnSandbox()
		? 'Wikipedia:Commissione arbitrale/ModelloDeliberazione/Sandbox'
		: 'Wikipedia:Commissione arbitrale/ModelloDeliberazione';
	const requestTemplate = isOwnSandbox()
		? 'RichiestaVAR/Sandbox'
		: 'RichiestaVAR';

	/**
	 * Verifica se è stato compilato solo il parametro "motivo" del
	 * template:RichiestaVAR passato in input.
	 * 
	 * @param {object} [request] Il template:RichiestaVAR da esaminare
	 * @return {boolean}
	 */
	function isRequestNew( request ) {
		const status = request.args.stato,
			resolutionLink = request.args[ 'link deliberazione' ],
			reason = request.args.motivo;

		return ( !status || !status.value.trim() ) &&
			( !resolutionLink || !resolutionLink.value.trim() ) &&
			( reason && reason.value.trim() );
	}

	/**
	 * Filtra i template:RichiestaVAR passati in input scartando le richieste vecchie.
	 * Restituisce il template il cui indice combacia con quello del pulsante
	 * cliccato dall'utente.
	 * 
	 * @param {number} [buttonIndex] L'indice del pulsante cliccato
	 * @param {object} [requests] Array di template:RichiestaVAR
	 * @return {object}
	 */
	function findRequest( buttonIndex, requests ) {
		return requests
			.filter( request => isRequestNew( request ) )
			.sort( ( a, b ) => {
				return a.span[ 0 ] > b.span[ 0 ] ? 1 : -1;
			} )[ buttonIndex ];
	}

	/**
	 * Restituisce la stringa passata in input dopo aver inserito un backslash
	 * davanti a ciascun carattere.
	 * 
	 * @param {string} [str] La stringa da manipolare
	 * @return {string}
	 */
	function escapeChars( str ) {
		return Array.from( str, c => '\\' + c ).join( '' );
	}

	/**
	 * Restituisce la data passata in input nel formato YYYY-MM-DD.
	 * 
	 * @param {object} [date] L'oggetto che rappresenta la data
	 * @return {string}
	 */
	function formatDate( date ) {
		const localeDateString = date.toLocaleDateString( 'it-IT', {
			year: 'numeric',
			month: '2-digit',
			day: '2-digit',
			timeZone: 'Europe/Rome'
		} );

		return localeDateString.substring( 6, 10 ) + '-'
			+ localeDateString.substring( 3, 5 ) + '-'
			+ localeDateString.substring( 0, 2 );
	}

	/**
	 * Restituisce il link a una pagina wiki.
	 * 
	 * @param {string} [title] Il titolo della pagina da linkare
	 * @return {string}
	 */
	function link( title ) {
		return `<a href=${ mw.util.getUrl( title ) }>${ title }</a>`;
	}

	/**
	 * Effettua una chiamata API per chiedere la modifica di una pagina.
	 * 
	 * @param {object} [customParams] I parametri della chiamata che si
	 *  integrano con quelli precompilati (o li sovrascrivono)
	 * @return {jQuery.Promise}
	 */
	function editContent( customParams ) {
		return new mw.Api( {
				parameters: {
					action: 'edit',
					format: 'json',
					watchlist: 'nochange'
				}
			} ).postWithToken( 'csrf', customParams );
	}

	/**
	 * Effettua una chiamata API per ottenere l'ID dell'ultima versione di una pagina.
	 * 
	 * @param {number} [pageid] L'ID della pagina
	 * @return {jQuery.Promise}
	 */
	function getCurrentRevisionId( pageid ) {
		return new mw.Api().get( {
				action: 'query',
				pageids: pageid,
				prop: 'revisions',
				rvprop: 'ids',
				format: 'json',
				formatversion: 2
			} );
	}

	/**
	 * Effettua una chiamata API per ottenere la versione desiderata di una pagina,
	 * compreso un array di tutti i template:RichiestaVAR in essa contenuti.
	 * 
	 * @param {number} [revid] L'ID della versione della pagina
	 * @return {jQuery.Promise}
	 */
	function getRevision( revid ) {
		return $.ajax( {
				url: '//itwikiapi.toolforge.org/v1/revisions',
				data: {
					revid: revid,
					rvprop: 'ids|content|timestamp',
					templatenames: requestTemplate
				}
			} );
	}

	/**
	 * Effettua una chiamata API per ottenere il conteggio delle pagine che
	 * sono state finora create sotto il path delle deliberazioni dell'arbcom.
	 * 
	 * @return {jQuery.Promise}
	 */
	function getResolutionCount() {
		const colonIndex = resolutionRootPage.indexOf( ':' ),
			nsName = resolutionRootPage.substring( 0, colonIndex ),
			pageName = resolutionRootPage.substring( colonIndex + 1 ),
			pageNameEscaped = escapeChars( pageName );

		const searchString = 'intitle:"' + pageName.replace( '"', '' ) + '"'
			+ ' intitle:/' + pageNameEscaped + '\\/[1-9][0-9]*/'
			+ ' -intitle:/' + pageNameEscaped + '\\/[1-9][0-9]*[^0-9]/'
			+ ' -intitle:/.+' + pageNameEscaped + '/';

		return new mw.Api().get( {
				action: 'query',
				list: 'search',
				srsearch: searchString,
				srnamespace: conf.wgNamespaceIds[ nsName.toLowerCase() ],
				srinfo: 'totalhits',
				srlimit: 1,
				srsort: 'create_timestamp_desc',
				format: 'json',
				formatversion: '2'
			} );
	}

	/**
	 * Inserisce il link alla deliberazione della Commissione arbitrale nel
	 * parametro "link deliberazione" del template:RichiestaVAR che corrisponde
	 * alla richiesta aperta.
	 * Restituisce il wikitesto modificato.
	 * 
	 * @param {string} [link] Il link alla deliberazione
	 * @param {object} [request] Il template della richiesta aperta
	 * @param {string} [wikitext] Il wikitesto da modificare
	 * @return {string}
	 */
	function insertResolutionLink( link, request, wikitext ) {
		let name, value, startIndex, endIndex;

		if ( request.args[ 'link deliberazione' ] ) {
			( { name, value } = request.args[ 'link deliberazione' ] );
			value = value.replace( /(\n?$)/, link + '$1' );
			[ startIndex, endIndex ] = request.args[ 'link deliberazione' ].span;
		} else {
			// aggiunge il parametro mantenendo la stessa spaziatura di "motivo"
			( { name, value } = request.args.motivo );
			name = name.replace( 'motivo', 'link deliberazione' );
			value = value.replace( value.trim(), link );
			startIndex = endIndex = request.span[ 1 ] - 2;
		}

		return wikitext.substring( 0, startIndex )
			+ `|${ name }=${ value }`
			+ wikitext.substring( endIndex );
	}

	/**
	 * Crea la pagina dove sarà pubblicata la deliberazione della Commissione
	 * arbitrale e la linka o include in tutte le pagine correlate.
	 * 
	 * @param {number} [buttonIndex] L'indice del pulsante cliccato
	 * @param {function} [progressMsgHandler] La funzione per mostrare progressi
	 * @param {function} [errorMsgHandler] La funzione per mostrare errori
	 * @param {function} [successMsgHandler] La funzione per mostrare successo
	 * @return {jQuery.Promise}
	 */
	function acceptRequest( buttonIndex, progressMsgHandler, errorMsgHandler, successMsgHandler ) {
		const currentRevisionIdPromise = getCurrentRevisionId( conf.wgArticleId );
		let revision, resolutionTitle, request, submitter;

		progressMsgHandler( mw.msg( 'fetchingRequestPage' ) );

		// scarica i dati della versione visualizzata della pagina che contiene
		// la richiesta
		return getRevision( conf.wgRevisionId ).then( result => {
			if ( !result.query.badrevids ) {
				revision = result.query.pages[ 0 ].revisions[ 0 ];
				request = findRequest( buttonIndex, revision.templates );
			}

			if ( !request ) {
				currentRevisionIdPromise.abort();
				return $.Deferred().reject( 'templatemissing' );
			}

			// ottiene l'ID dell'ultima versione della pagina che contiene la richiesta
			return currentRevisionIdPromise;
		} ).then( result => {
			// verifica che la pagina non sia stata modificata nel frattempo
			if (
				result.query.pages[ 0 ].missing ||
				result.query.pages[ 0 ].revisions[ 0 ].revid !== revision.revid
			) {
				return $.Deferred().reject( 'oldrevision' );
			}

			// ottiene il conteggio delle deliberazioni che sono state create finora
			return getResolutionCount();
		} ).then( result => {
			progressMsgHandler( mw.msg( 'creatingResolutionPage' ) );

			let requestDate,
				counterparty,
				resolutionNumber = result.query.searchinfo.totalhits + 1;

			// verifica se il richiedente è un utente specifico
			if ( conf.wgRelevantUserName && !isOwnSandbox() ) {
				submitter = conf.wgRelevantUserName;
			} else if ( request.args.richiedente ) {
				submitter = request.args.richiedente.value.trim() || null;
			}

			if ( request.args[ 'data richiesta' ] ) {
				requestDate = request.args[ 'data richiesta' ].value.trim() || null;
			}

			if ( request.args.controparte ) {
				counterparty = request.args.controparte.value.trim() || null;
			}

			const editContentPromise = $.Deferred(),
				loopStartDate = new Date();

			( function loop() {
				resolutionTitle = `${ resolutionRootPage }/${ resolutionNumber }`;

				// compila il modello predefinito delle deliberazioni dell'arbcom
				const text = '{{subst:' + resolutionTemplate
					+ '|richiedente=' + ( submitter || 'Richiesta comunitaria' )
					+ '|data richiesta=' + ( requestDate || '' )
					+ '|motivo=' + request.args.motivo.value.trim()
					+ '|oldid motivo=' + revision.revid
					+ '|controparte=' + ( counterparty || '' )
					+ '|data presa in carico=' + formatDate( new Date() )
					+ '}}';

				// crea la pagina dove sarà pubblicata la deliberazione
				editContent( {
					title: resolutionTitle,
					text: text,
					summary: mw.msg( 'resolutionPageEditSummary' ),
					createonly: 1,
					watchlist: 'watch'
				} ).then( result => {
					progressMsgHandler( mw.msg( 'resolutionPageCreated', link( resolutionTitle ) ) );
					editContentPromise.resolve( result );
				} ).fail( code => {
					if ( code !== 'articleexists' ) {
						// si è verificato un errore inatteso
						editContentPromise.reject( code );
					} else if ( ( new Date() - loopStartDate ) < 10000 ) {
						// prova a creare la pagina con un numero più alto se per
						// qualche motivo ne esiste già una con questo numero
						resolutionNumber += 1;
						loop();
					} else {
						// stoppa il loop dopo 10 secondi (dovrebbe succedere solo se
						// la connessione è lenta o se ci sono fin troppe anomalie
						// nella numerazione delle deliberazioni già pubblicate)
						editContentPromise.reject( 'resolutionpagecreationtimeout' );
					}
				} );
			}() );

			return editContentPromise;
		} ).then( () => {
			progressMsgHandler( mw.msg( 'transcludingResolutionPage', link( resolutionRootPage ) ) );

			// include la pagina della deliberazione nella pagina di servizio dove sono
			// elencate le deliberazioni recenti o non ancora pubblicate
			return editContent( {
				title: resolutionRootPage,
				appendtext: `\n\n{{${ resolutionTitle }}}`,
				summary: submitter
					? mw.msg( 'resolutionRootPageEditSummary-userRequest', submitter )
					: mw.msg( 'resolutionRootPageEditSummary-communityRequest' )
			} );
		} ).then( () => {
			progressMsgHandler( mw.msg( 'notifyingSubmitter' ) );

			// aggiorna il template:RichiestaVAR compilando il parametro
			// "link deliberazione" col nome della pagina appena creata
			const text = insertResolutionLink(
				resolutionTitle,
				request,
				revision.slots.main.content
			);

			// pubblica la modifica
			return editContent( {
				pageid: conf.wgArticleId,
				text: text,
				summary: mw.msg( 'requestPageEditSummary', resolutionTitle ),
				starttimestamp: revision.timestamp,
				baserevid: revision.revid
			} );
		} ).done( () => {
			successMsgHandler( mw.msg( 'success' ) );
		} ).fail( code => {
			let errorText = mw.msg( 'errorOccurred' ) + ' ';

			switch( code ) {
				case 'articleexists':
				case 'editconflict':
				case 'oldrevision':
				case 'templatemissing':
					errorText += mw.msg( code );
					break;
				case 'resolutionpagecreationtimeout':
					errorText += mw.msg( code, link( resolutionTitle ) );
					break;
				default:
					errorText += mw.msg( 'unknownError', code );
			}

			errorMsgHandler( errorText );
		} );
	}

	/**
	 * Dà il via alle operazioni e le mostra in una finestra di dialogo.
	 * 
	 * @param {number} [buttonIndex] L'indice del pulsante cliccato
	 * @param {OO.ui.WindowManager} [windowManager] Il gestore della finestra
	 */
	function onConfirmButtonClick( buttonIndex, windowManager ) {
		// crea la finestra che mostra le operazioni in corso
		const ProgressDialog = require( 'ext.gadget.ProgressDialog' );
		const progressDialog = new ProgressDialog( {
			closeButtonLabel: mw.msg( 'closeButtonLabel' ),
			dialogTitle: mw.msg( 'dialogTitle' )
		} );

		$( 'body' ).append( windowManager.$element );
		windowManager.addWindows( [ progressDialog ] );
		windowManager.openWindow( 'progressDialog' );

		// esegue le operazioni tenendo aggiornata la finestra
		acceptRequest(
			buttonIndex,
			text => progressDialog.appendProgressMsg( text ),
			text => progressDialog.appendErrorMsg( text ),
			text => progressDialog.appendSuccessMsg( text )
		).always(
			() => progressDialog.showCloseButton()
		);
	}

	/**
	 * Mostra un prompt di conferma dopo aver verificato che non siano state
	 * già avviate le operazioni.
	 * 
	 * @param {object} [event] L'oggetto che rappresenta l'evento del clic
	 */
	function onAcceptButtonClick( event ) {
		event.preventDefault();

		const { buttonIndex, windowManager } = event.data;

		if ( windowManager.isElementAttached() ) {
			// apre la finestra delle operazioni in corso se esiste già
			windowManager.openWindow( 'progressDialog' );
		} else {
			// chiede conferma all'utente se deve iniziare le operazioni
			OO.ui.confirm( mw.msg( 'prompt' ) ).done( confirmed => {
				if ( confirmed ) {
					onConfirmButtonClick( buttonIndex, windowManager );
				}
			} );
		}
	}

	/**
	 * Verifica se l'utente che sta usando l'accessorio è un membro dell'arbcom.
	 * 
	 * @return {boolean}
	 */
	function isUserArbitrator() {
		return conf.wgUserGroups.includes( 'arbcom' ) ||
			conf.wgUserId === 2525053; // account di servizio dell'arbcom
	}

	/**
	 * Verifica se la pagina visualizzata è la pagina di una UP.
	 * 
	 * @return {boolean}
	 */
	function isUP() {
		return conf.wgPageName.startsWith( 'Wikipedia:Utenti_problematici/' ) &&
			conf.wgArticleId !== 0;
	}

	/**
	 * Verifica se la pagina visualizzata è la pagina di discussione di un utente.
	 * 
	 * @return {boolean}
	 */
	function isTalkPage() {
		return conf.wgNamespaceNumber === 3 &&
			conf.wgRelevantUserName !== null &&
			conf.wgArticleId !== 0;
	}

	/**
	 * Verifica se la pagina visualizzata è una sandbox dell'utente che sta
	 * usando l'accessorio.
	 * 
	 * @return {boolean}
	 */
	function isOwnSandbox() {
		return conf.wgNamespaceNumber === 2 &&
			conf.wgRelevantUserName === conf.wgUserName;
	}

	/**
	 * Verifica se la pagina visualizzata rientra fra quelle dove è previsto che
	 * sia eseguito l'accessorio.
	 * 
	 * @return {boolean}
	 */
	function isPageValid() {
		return isOwnSandbox() || isTalkPage() || isUP();
	}

	$( () => {
		const acceptButton = $( '.pulsante-prendi-in-carico-richiesta-var' );

		// termina l'esecuzione se riscontra anomalie
		if ( !isPageValid() || !isUserArbitrator() || !acceptButton.length ) {
			return;
		}

		// aspetta il corretto caricamento delle dipendenze prima di procedere
		mw.loader.using( dependencies ).done( () => {
			// carica i messaggi di sistema dell'accessorio
			mw.messages.set( require( './PrendiInCaricoRichiestaVAR-Messages.json' ) );

			// modifica il comportamento del pulsante "Prendi in carico"
			acceptButton.each( ( i, el ) => {
				const windowManager = new OO.ui.WindowManager();

				$( el ).on( 'click', {
					buttonIndex: i,
					windowManager: windowManager
				}, onAcceptButtonClick );
			} );
		} );
	} );
}( mediaWiki, jQuery ) );