User:Ugochimobi/SortedPropertiesUpdater.js

// // Quick script to Simplify updating MediaWiki:Wikibase-SortedProperties // Thanks to User:DannyS712's from Wikidata (https://www.wikidata.org/w/index.php?title=User:DannyS712/SortedPropertiesUpdater.js&diff=1474248672&oldid=1474245859) // @maintainer Ugochimobi as at 4/2/2022 // For sorting external ids alphabetically, copy the block, split by newline, and use /* var unsortedText = `(dump without * or #)`; var unsortedRows = unsortedText.split( '\n' ); unsortedRows.sort(   function ( row1, row2 ) {        row1Info = row1.match( /^P\d+ \((.*?)\)$/ );        row2Info = row2.match( /^P\d+ \((.*?)\)$/ );        if ( !row1Info || !row2Info ) {            return 0;        }        return row1Info[1].localeCompare( row2Info[1] );    } ); unsortedRows.join( '\n# ' ); /* jshint maxerr: 999 */ $( => { const SortedPropertiesUpdater = {}; window.SortedPropertiesUpdater = SortedPropertiesUpdater;

// Included in the edit summary SortedPropertiesUpdater.version = '1.2';

SortedPropertiesUpdater.init = function { window.document.title = 'SortedProperties updater'; $( '#firstHeading' ).text( 'SortedProperties updater' ); mw.loader.using(		[ 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'mediawiki.api', 'mediawiki.util' ],		SortedPropertiesUpdater.run	); };

SortedPropertiesUpdater.onErrHandler = function { // Shared error handler alert( 'Something went wrong' ); console.log( arguments ); };

SortedPropertiesUpdater.startedProcessing = false;

// Map technical name to that for the section `Other properties with datatype "?"` // Key: string data type from api (was stored in propertyDataTypesCache) // Value: string data type for section name SortedPropertiesUpdater.datatypeMap = { 'commonsMedia': 'Commons media file', // Omitting external id intentionally, handled separately 'geo-shape': 'geo-shape', 'globe-coordinate': 'geographic coordinates', 'math': 'math', 'monolingualtext': 'monolingual text', 'musical-notation': 'musical-notation', 'quantity': 'quantity', 'string': 'string', 'tabular-data': 'tabular data', 'time': 'point in time', 'url': 'url', 'wikibase-form': 'wikibase-form', 'wikibase-item': 'item', 'wikibase-lexeme': 'wikibase-lexeme', 'wikibase-property': 'wikibase-property', 'wikibase-sense': 'wikibase-sense' };

SortedPropertiesUpdater.run = function { var latestPropertyWidget = new OO.ui.TextInputWidget; var latestPropertyLayout = new OO.ui.FieldLayout(		latestPropertyWidget,		{ label: 'Latest property (in the form `xxx` for Pxxx)' }	); var autoUpdateWidget = new OO.ui.CheckboxInputWidget; var autoUpdateLayout = new OO.ui.FieldLayout(		autoUpdateWidget,		{ label: 'Automatically update (BE CAREFUL AND DOUBLE CHECK)' }	); var submit = new OO.ui.ButtonInputWidget( { 		label: 'View updates',		flags: [ 'primary', 'progressive' ]	} ); submit.on( 'click', function {		// CONVERT YOUR WIDGETS TO INPUT		var submittedValues = {			latestProperty: latestPropertyWidget.getValue,			autoUpdate: autoUpdateWidget.isSelected		};		console.log( submittedValues );		SortedPropertiesUpdater.onSubmit( submittedValues );	} ); $( window ).on( 'keypress', function ( e ) {		// press enter to start		if ( e.which == 13 ) {			submit.simulateLabelClick;		}	} ); var fieldSet = new OO.ui.FieldsetLayout( { 		label: $( ' ' )			.append( 'View and implement updates needed for ' )			.append( $( '' ) .attr( 'href', '/wiki/MediaWiki:Wikibase-SortedProperties' ) .attr( 'target', '_blank' ) .text( 'MediaWiki:Wikibase-SortedProperties' ) )	} );	fieldSet.addItems( [		latestPropertyLayout,		autoUpdateLayout,		new OO.ui.FieldLayout( submit )	] );

var $results = $( ' ' ) .attr( 'id', 'SortedPropertiesUpdater-results' ); $( '#mw-content-text' ).empty.append(		fieldSet.$element,		$( ' ' ),		$results	); SortedPropertiesUpdater.autoFillLatestProperty( latestPropertyWidget ); };

SortedPropertiesUpdater.autoFillLatestProperty = function ( inputWidget ) { SortedPropertiesUpdater.getLatestProperty.then(		function ( latestProperty ) {			console.log( 'Got latest property: ' + latestProperty );			if ( SortedPropertiesUpdater.startedProcessing === false ) {				inputWidget.setValue( latestProperty );				console.log( 'Updated widget accordingly' );			}		}	); }; SortedPropertiesUpdater.getLatestProperty = function { return new Promise(		function ( resolve ) {			new mw.Api.get( { action: 'query', format: 'json', formatversion: 2, list: 'recentchanges', rcdir: 'older', rcnamespace: 862, // property rcprop: 'title', rclimit: 1, // just the latest rctype: 'new' // looking for new property creations } ).then( function ( response ) { console.log( response ); var propertyTitle = response.query.recentchanges[0].title; var propertyId = propertyTitle.replace( 'Property:P', '' ); resolve( propertyId ); },				SortedPropertiesUpdater.onErrHandler );		}	); };

// Calls itself recursively until everything is retrieved SortedPropertiesUpdater.getRawLabels = function ( propertyIds ) { // TODO handle non-admins running this script with 50 instead of 500 return new Promise(		function ( resolve ) {			new mw.Api.get( { action: 'wbgetentities', formatversion: 2, ids: propertyIds.slice( 0, 500 ), languages: 'en', props: 'labels' } ).then( function ( response ) { console.log( response ); var labels = response.entities; if ( propertyIds.length > 500 ) { // recursive SortedPropertiesUpdater.getRawLabels( propertyIds.slice( 500 ) ).then(							function ( extraLabels ) {								var bothLabels = $.extend( {}, labels, extraLabels );								resolve( bothLabels );							},							SortedPropertiesUpdater.onErrHandler						); } else { resolve( labels ); }				},				SortedPropertiesUpdater.onErrHandler );		}	); };

SortedPropertiesUpdater.propertyDataTypesCache = {}; SortedPropertiesUpdater.getRowsForProperties = function ( labelInfo ) { var properties = Object.keys( labelInfo ); var rows = []; var propertyInfo; var propertyRow; properties.forEach( function ( propertyId ) {		propertyInfo = labelInfo[ propertyId ];		if ( propertyInfo.missing !== undefined ) {			return;		}		propertyRow = propertyInfo.id + ' (' + propertyInfo.labels.en.value + ')';		rows.push( propertyRow );		// For later		SortedPropertiesUpdater.propertyDataTypesCache[ propertyInfo.id ] = propertyInfo.datatype;	} ); console.log( rows ); return rows; };

SortedPropertiesUpdater.currentTextCache = false;

SortedPropertiesUpdater.getCurrentSortedProperties = function { return new Promise(		function ( resolve ) {			new mw.Api.get( { action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: 'MediaWiki:Wikibase-SortedProperties', formatversion: 2 } ).then( function ( response ) { console.log( response ); var text = response.query.pages[0].revisions[0].slots.main.content; SortedPropertiesUpdater.currentTextCache = text; // for use later var rows = text.split( '\n' ); rows = rows.filter(						function ( row ) {							// Ignore the section headings, etc.							// Some entries start with *, others with #							return ( row.startsWith( '* P' ) || row.startsWith( '# P' ) );						}					); rows = rows.map(						function ( row ) {							// remove the first two characters, the * or # and the space							return row.substring( 2 );						}					); resolve( rows ); },				SortedPropertiesUpdater.onErrHandler );		}	); };

SortedPropertiesUpdater.compareWithCurrent = function ( goodRows, autoUpdate ) { SortedPropertiesUpdater.getCurrentSortedProperties.then( function( currentSortedProperties ) {		console.log( currentSortedProperties );		// Handle changes in labels first		// Map of property id to label		var currentLabels = {}, goodLabels = {};		var rowInfo;		currentSortedProperties.forEach( function ( currentRow ) { rowInfo = currentRow.match( /^(P\d+) \((.*?)\)$/ ); if ( rowInfo ) { currentLabels[ rowInfo[1] ] = rowInfo[ 2 ]; } else { console.log( 'Odd row in currentSortedProperties: ' + currentRow ); }			}		);		goodRows.forEach( function ( goodRow ) { rowInfo = goodRow.match( /^(P\d+) \((.*?)\)$/ ); if ( rowInfo ) { goodLabels[ rowInfo[1] ] = rowInfo[ 2 ]; } else { console.log( 'Odd row in goodRows: ' + goodRow ); }			}		);		var labelChanges = [];		var labelChangesData = {};		var rowsToRemove = [];		var rowsToAdd = [];		var rowsToAddData = {};		var currentLabelForProperty;		Object.keys( currentLabels ).forEach( function ( propertyId ) { currentLabelForProperty = currentLabels[ propertyId ]; if ( goodLabels[ propertyId ] === undefined ) { // No good label for this property, so it was deleted rowsToRemove.push( propertyId + ' (' + currentLabelForProperty + ')' ); return; }				// There is a label for this property, compare it				if ( goodLabels[ propertyId ] !== currentLabelForProperty ) { labelChangesData[ propertyId ] = [ currentLabelForProperty, goodLabels[ propertyId ] ]; labelChanges.push( propertyId + ': ' + currentLabelForProperty + ' => ' + goodLabels[ propertyId ] ); }			}		);		Object.keys( goodLabels ).forEach( function ( propertyId ) { // We have a good label. If there is no prior label, it means its a new property to add if ( currentLabels[ propertyId ] === undefined ) { rowsToAdd.push( propertyId + ' (' + goodLabels[ propertyId ] + ')' ); rowsToAddData[ propertyId ] = goodLabels[ propertyId ]; }			}		);		var sortedRowsToAdd = SortedPropertiesUpdater.formatRowsToAdd( rowsToAddData );		var $res = $( '#SortedPropertiesUpdater-results' );		$res.append( $( ' ' ).append( 'Checked against current rows. Results:' ) );		$res.append( $( ' ' ).text(				'Rows to change:\n' + labelChanges.join( '\n' ) +				'\n\n' +				'Rows to remove:\n' + rowsToRemove.join( '\n' ) +				'\n\n' +				'Rows to add:\n' + rowsToAdd.join( '\n' ) +				'\n\n' +				'Sorted rows to add info:\n' + sortedRowsToAdd			) );		SortedPropertiesUpdater.makeNewContent( labelChangesData, rowsToAddData, rowsToRemove, autoUpdate );	} ); };

SortedPropertiesUpdater.formatRowsToAdd = function ( rowsToAddData ) { var resultText = ''; if ( rowsToAddData ) { // object with keys as property ids, values as the labels // use SortedPropertiesUpdater.propertyDataTypesCache var propertiesToAddByType = {}; Object.keys( rowsToAddData ).forEach(			function ( propertyIdToAdd ) {				var propertyType = SortedPropertiesUpdater.propertyDataTypesCache[ propertyIdToAdd ];				if ( propertiesToAddByType[ propertyType ] === undefined ) {					propertiesToAddByType[ propertyType ] = [];				}				propertiesToAddByType[ propertyType ].push( '* ' + propertyIdToAdd + ' (' + rowsToAddData[ propertyIdToAdd ] + ')' );			}		);		Object.keys( propertiesToAddByType ).forEach(			function ( dataType ) {				resultText = resultText + '; Data type ' + dataType + '\n' + propertiesToAddByType[ dataType ].join( '\n' ) + '\n';			}		); }	return resultText; };

SortedPropertiesUpdater.makeNewContent = function ( labelChangesData, rowsToAddData, rowsToRemove, autoUpdate ) { var newText = SortedPropertiesUpdater.currentTextCache; var replacementRegex; var replacementInfo; Object.keys( labelChangesData ).forEach(		function ( propertyNeedingLabelChange ) {			// key is the property id, value is an array of the current label and the new label			replacementInfo = labelChangesData[ propertyNeedingLabelChange ];			replacementRegex = new RegExp( propertyNeedingLabelChange + ' \\(' + mw.util.escapeRegExp( replacementInfo[ 0 ] ) + '\\)\\n' );			newText = newText.replace( replacementRegex, propertyNeedingLabelChange + ' (' + replacementInfo[ 1 ] + ')\n' );		}	);	if ( Object.keys( rowsToAddData ).length > 0 ) { // filter out new external ids, those are sorted into alphabetical order section seperately var newExternalIds = []; Object.keys( rowsToAddData ).forEach(			function ( propertyIdToAdd ) {				var propertyType = SortedPropertiesUpdater.propertyDataTypesCache[ propertyIdToAdd ];				if ( propertyType === 'external-id' ) {					// add the # here so that we can be consistent in the handling with the existing entries					newExternalIds.push( '# ' + propertyIdToAdd + ' (' + rowsToAddData[ propertyIdToAdd ] + ')' );					delete rowsToAddData[ propertyIdToAdd ];				}			}		); console.log( newExternalIds ); // retrieve current external ids in alphabetical order var currentExternalIdsABCmatch = newText.match(			/\n([\s\S]+?)\n/		); if ( currentExternalIdsABCmatch && currentExternalIdsABCmatch[1] ) { var currentExternalIdsABC = currentExternalIdsABCmatch[1]; var unsortedExternalIds = currentExternalIdsABC.split( '\n' ); unsortedExternalIds = unsortedExternalIds.concat( newExternalIds ); unsortedExternalIds.sort(				function ( row1, row2 ) {					row1Info = row1.match( /^# P\d+ \((.*?)\)$/ );					row2Info = row2.match( /^# P\d+ \((.*?)\)$/ );					if ( !row1Info || !row2Info ) {						return 0;					}					return row1Info[1].localeCompare( row2Info[1] );				}			); var updatedSortedExternalIds = unsortedExternalIds.join( '\n' ); newText = newText.replace(				/\n([\s\S]+?)\n/,				'\n' + updatedSortedExternalIds + '\n'			); }		if ( Object.keys( rowsToAddData ).length > 0 ) { // need check rowsToAddData again since maybe all of the new properties were external ids handled separately // Try to add to relevant sections by datatype Object.keys( rowsToAddData ).forEach(				function ( propertyIdToAdd ) {					var propertyType = SortedPropertiesUpdater.propertyDataTypesCache[ propertyIdToAdd ];					var dataTypeInSection = SortedPropertiesUpdater.datatypeMap[ propertyType ];					if ( !dataTypeInSection ) {						// Anything I missed, or new properties being added...						return;					}					var sectionRegex = new RegExp( '(=== Other properties with datatype "' + dataTypeInSection + '" ===(?:\\n\\* P\\d+ .*)+)' );					var sectionMatch = newText.match( sectionRegex );					if ( !sectionMatch || !sectionMatch[1] ) {						// Missing section, or I messed up						return;					}					// Add entry to relevant section					newText = newText.replace( sectionRegex, sectionMatch[1] + '\n* ' + propertyIdToAdd + ' (' + rowsToAddData[ propertyIdToAdd ] + ')' );					// Remove from unsorted					delete rowsToAddData[ propertyIdToAdd ];				}			); }		if ( Object.keys( rowsToAddData ).length > 0 ) { // need check rowsToAddData again since maybe all of the new properties were external ids handled separately, or			// were properly sorted by section // the "Unsorted properties" heading might already be there, if so don't add another if ( !( newText.match( /==\s*Unsorted properties\s*==/i ) ) ) { // only 1 newline here, so that we can ensure there is always a newline before the new entries if there // was a heading already newText = newText + '\n\n== Unsorted properties ==\n'; }			newText = newText + '\n' + SortedPropertiesUpdater.formatRowsToAdd( rowsToAddData ); }	}

// Automatically remove deleted properties var removalRegex; rowsToRemove.forEach(		function ( deletedProperty ) {			removalRegex = new RegExp( '\\n[*#] ' + mw.util.escapeRegExp( deletedProperty ) );			newText = newText.replace( removalRegex, ""			);		}	);	$( '#SortedPropertiesUpdater-results' ).append(		$( ' ' ).append( 'An updated text is available to copy from the console' )	); console.log( newText ); if ( autoUpdate ) { SortedPropertiesUpdater.autoUpdate( newText ); } };

SortedPropertiesUpdater.autoUpdate = function ( newText ) { mw.notify( 'Automatically updating!', { autoHide: false } ); var params = { action: 'edit', title: 'MediaWiki:Wikibase-SortedProperties', text: newText, summary: 'Automatically updating via User:Ugochimobi/SortedPropertiesUpdater.js (version ' + SortedPropertiesUpdater.version + ')', nocreate: true, // sanity assert: 'user', // non users shouldn't be able to edit MediaWiki namespace, but why not? format: 'json', formatversion: 2 };	console.log( params ); new mw.Api.postWithEditToken( params ).then(		function ( response ) {			console.log( response );			mw.notify( 'Finished auto update', { autoHide: false } );		},		SortedPropertiesUpdater.onErrHandler	); };

SortedPropertiesUpdater.onSubmit = function ( inputs ) { SortedPropertiesUpdater.startedProcessing = true;

var $res = $( '#SortedPropertiesUpdater-results' ); $res.empty; console.log( inputs ); var latestProperty = inputs.latestProperty; $res.append(		$( ' ' ).append( 'Checking the api for labels of properties up to P' + latestProperty )	);	// Array from P1 to P var propertyIds = []; for ( var iii = 1; iii <= latestProperty; iii++ ) { propertyIds.push( 'P' + iii ); }	SortedPropertiesUpdater.getRawLabels( propertyIds ).then(		function ( labelInfo ) {			console.log( labelInfo );			$res.append( $( ' ' ).append( 'Api calls done, now converting to rows' ) );			var goodRows = SortedPropertiesUpdater.getRowsForProperties( labelInfo );			$res.append( $( ' ' ).append( '...now comparing to current content' ) );			SortedPropertiesUpdater.compareWithCurrent( goodRows, inputs.autoUpdate );		}	); };

});

$( document ).ready( => {	mw.loader.using( [ 'mediawiki.util' ], function { mw.util.addPortletLink(				'p-tb',				'/wiki/Special:BlankPage/SortedPropertiesUpdater',				'View updates for SortedProperties'			); }	);	if ( mw.config.get( 'wgNamespaceNumber' ) === -1 ) {		const page = mw.config.get( 'wgCanonicalSpecialPageName' );		if ( page === 'Blankpage' ) {			const page2 = mw.config.get( 'wgTitle' ).split( '/' );			if ( page2[1] && page2[1] === 'SortedPropertiesUpdater' ) {				window.SortedPropertiesUpdater.init;			}		}	} });

//