Jump to content

User:Ahecht/Scripts/draft-sorter.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Ahecht (talk | contribs) at 20:39, 29 June 2021 (use async functions, other code cleanup and simplification). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
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.
//jshint maxerr:512
//jshint esnext:false
//jshint esversion:8

//Based on [[User:Enterprisey/draft-sorter.js]] <nowiki>
( function ( $, mw ) { mw.loader.using( ["mediawiki.api", "jquery.chosen", "oojs-ui-core"], function () {
	mw.loader.load( "mediawiki.ui.input", "text/css" );

	if ( mw.config.get( "wgNamespaceNumber" ) !== 118 ) { 
		if ( mw.util.getParamValue('draftsorttrigger') ) {
			// "Next draft" was clicked, but we ended up on a non-draft page
			nextDraft();
			return;
		} else {
			return;
		}
	}

	var portletLink = mw.util.addPortletLink("p-cactions", "#", "Sort draft (sandbox)", "pt-draftsort", "Manage WikiProject tags");
	$( portletLink ).click( function ( e ) {
		e.preventDefault();

		// If it's already there, don't duplicate
		if ( $( "#draft-sorter-wrapper" ).length ) { return; }
		
		// Configure defaults
		var templateCache = mw.config.get("wgFormattedNamespaces")[2]+":"+mw.config.get("wgUserName")+"/Scripts/draft-sorter.json";
		//var templateCache = "Wikipedia:WikiProject Articles for creation/WikiProject templates.json";
		var templateIncludelistName = "Wikipedia:WikiProject Articles for creation/WikiProject templates.json/includelist.json";
		var templateBlocklistName = "Wikipedia:WikiProject Articles for creation/WikiProject templates.json/blocklist.json";
		var wikiProjectMainCategory = "Category:WikiProject banner wrapper templates";
		var wikiProjectCategories = [ // other WikiProject categories not included in wikiProjectMainCategory
			"Category:WikiProject banners with quality assessment",
			"Category:WikiProject banners without quality assessment",
			"Category:WikiProject banner templates not based on WPBannerMeta",
			"Category:Inactive WikiProject banners",
			"Category:WikiProject banner wrapper templates",
		];
		
		// Define the form
		var form = $( "<div>" )
			.attr( "id", "draft-sorter-wrapper" )
			.css( { "background-image": "url(/media/wikipedia/commons/e/e2/OOjs_UI_icon_tag-ltr-progressive.svg)",
					"background-repeat": "no-repeat",
					"background-position-y": "center",
					"background-size": "50px",
					"min-height": "50px",
					"margin": "1em auto",
					"border": "thin solid #BBB",
					"padding": "0.5em 50px",
					"display": "inline-block",
					"border-radius": "0.25em"
			} ).append( $( "<span>" )
				.text( "Loading form..." )
				.css( "color", "gray" )
			);
		// Add the form to the page
		form.insertAfter( "#jump-to-nav" );

		var select = $( "<select>" )
			.attr( "id", "draft-sorter-form" )
			.attr( "multiple", "multiple" );
		
		var submitButton = new OO.ui.ButtonWidget ()
				.setLabel( "Submit" )
				.setFlags( [ 'primary', 'progressive' ] )
				.on("click", function ( e ) { submit(); } );
		
		var cancelButton = new OO.ui.ButtonWidget ()
				.setLabel( "Cancel" )
				.setFlags( ["destructive"] )
				.on("click", function( e ) {
					$( "#draft-sorter-wrapper" ).remove();
					window.location.replace( window.location.href.replace("draftsorttrigger=y","") );
				} );

		var nextButton = new OO.ui.ButtonWidget ()
			.setIcon( "next" )
			.setLabel( "Skip" )
			.on("click", function ( e ) { nextDraft(); } );

		// Determine what templates are already on the talk page
		var existingProjects = [];
		var wikiprojects = {};

		new mw.Api().get( {
				action: "query",
				titles: "Draft talk:" + mw.config.get( "wgTitle" ),
				generator: "templates",
				redirects: "1",
				gtllimit: "max",
		} ).done (function (data) {
			if (data && data.query && data.query.pages) {
				$.each(data.query.pages, function (i) {
					var item = data.query.pages[i].title.match(/^Template:(WikiProject\s[^\/]*)$/i);
					if (item && item[1] && item[1] != "WikiProject banner shell") {
						existingProjects.push(item[1]);
					}
				} );
			}
			console.log( "Project templates found on talk page: ");
			console.log( existingProjects );
			fetchJSONList(templateCache).then( (cachedList) => {
				checkTemplateCache(cachedList);
			} );
		} ).fail (function() {
			console.log("Retrieving project templates from talk page failed.");
			fetchJSONList(templateCache).then( (cachedList) => {
				checkTemplateCache(cachedList);
			} );
		});
		
		predicts = [];
		
		async function fetchJSONList(listName) {
			var parsedList = {}, listData;
			
			var query = {
				action:'parse',
				prop:'wikitext',
				page: listName,
				formatversion: '2',
				origin: '*'
			};
			
			try {
				listData = await new mw.Api().get( query );
			} catch (jsonerror) {
				console.warn("Unable to fetch contents of " + listName + ":");
				console.log(jsonerror);
			}
			
			if (listData && listData.parse && listData.parse.wikitext) {
				try {
					parsedList = JSON.parse(listData.parse.wikitext);
				} catch (jsonerror) {
					console.warn("Error parsing JSON list " + listName + ":");
					console.log(jsonerror);
				}
			}
		
			return parsedList;
		}
		
		function checkTemplateCache(cachedList) {
			console.log("Retrieved cache from "+templateCache+".");
			if (cachedList._timestamp) {
				var cacheTimestamp = new Date(cachedList._timestamp);
				delete cachedList._timestamp;
				//Check if cache timestamp is more than 24 hours old
				if ( isFinite(cacheTimestamp) && (Date.now() - cacheTimestamp < 86400000) ) {
					console.log("Cache timestamp \"" + cacheTimestamp + "\" OK.");
					wikiprojects = cachedList;
					constructForm();
				} else {
					console.warn("Cache timestamp \"" + cacheTimestamp + "\" too old. Rebuilding...");
					getTemplateCategories();
				}
			} else {
				console.warn(wikiprojects);
				console.warn("Cannot find timestamp in cache. Rebuilding...");
				getTemplateCategories();
			}
		}
		
		function writeTemplateCache() {
			wikiprojects = Object.keys(wikiprojects).sort().reduce(
				(obj, key) => { 
					obj[key] = wikiprojects[key]; 
					return obj;
				}, 
				{}
			);
			
			wikiprojects._timestamp = new Date().toJSON();
			
			var params = { action: 'edit',
				title: templateCache,
				text: JSON.stringify(wikiprojects),
				summary: "Update WikiProject cache ([[User:Ahecht/Scripts/draft-sorter|draft-sorter]])",
				watchlist:"unwatch"
			};
			
			new mw.Api().postWithToken("csrf", params ).done( function(reslt) {
				console.log(templateCache + " updated:");
				console.log(reslt);
				constructForm();
			} ).fail( function(reslt) {
				console.error("Error updating " + templateCache + ":");
				console.error(reslt);
				constructForm();
			} );
		}
		
		function nextDraft() {
			// Special:RandomInCategory isn't random, so this function is a
			// better substitute.
			draftList = [];
			console.log ("Fetching drafts from API");
			if (nextButton) {
				nextButton.setLabel( "Loading..." ).setDisabled( true );
			}
			getDrafts();
			
			function getDrafts(cont) { // Recursively call API
				var query = {
					action: "query",
					list: "categorymembers",
					cmtitle: "Category:Pending_AfC_submissions",
					cmprop: "title",
					cmnamespace: "118",
					cmtype: "page",
					cmlimit: "max"
				};
				if (cont) {
					query = Object.assign(query, cont);
				}
				new mw.Api().get( query )
					.done (function (data) {
						if (data && data.query && data.query.categorymembers) { //API query returned pages
							data.query.categorymembers.forEach(function(item) {
								draftList.push( item.title );
							} );
						}
						if (data && data.continue) { //More results are available
							getDrafts(data.continue);
						} else { // Redirect to random page
							console.log("Done fetching drafts!");
							window.location.href = mw.config.get( "wgServer" )
								+ "/wiki/"
								+ draftList[Math.random() * draftList.length | 0]
								+ "?draftsorttrigger=y";
						}
					} ).fail (function(error) { // Use Special:RandomInCategory
						console.error("Error getting list of drafts:");
						console.error(error);
						window.location.href = mw.config.get( "wgServer" )
							+ "/wiki/Special:RandomInCategory/Pending_AfC_submissions?draftsorttrigger=y";
					} );
				return;
			}
		}
		
		function showPredicts() {
			$( "#draft-sorter-status" ).append( "<li>Suggested categories from <a href=\"https://www.mediawiki.org/wiki/ORES#Topic_routing\">ORES</a>:<ul id=\"draft-sorter-suggest\"></ul></li>" );
			predicts.forEach( function(item) { 
				var addLink = $( "<a>" )
					.text("add")
					.click( function() {
						$( select ).val( 
							$( select ).val().concat( [ "WikiProject " + item ] )
						).trigger("chosen:updated");
					} );
				var singularItem = item.replace(/s$/, '');
				if( !existingProjects.includes( "WikiProject " + item ) 
					&& wikiprojects[item]
				) { //Prediction matches a WikiProject and doesn't already exist
					$( "#draft-sorter-suggest" ).append( $( "<li>" )
						.append( item + " (" )
						.append( addLink )
						.append( ")" )
					);
				} else if( singularItem != item
					&& !existingProjects.includes( "WikiProject " + singularItem ) 
					&& wikiprojects[singularItem]
				) { //Singular form of prediction matches a WikiProject and doesn't exist
					addLink.click( function() {
						$( select ).val( 
							$( select ).val().concat( [ "WikiProject " + singularItem ] )
						).trigger("chosen:updated");
					} );
					$( "#draft-sorter-suggest" ).append( $( "<li>" )
						.append( singularItem + " (" )
						.append( addLink )
						.append( ")" )
					);
				} else { //Prediction doesn't match a WikiProject or already exists
					$( "#draft-sorter-suggest" ).append( 
						$( "<li>" ).append( item  )
					);
				}
			} );
			return;
		}
		
		function getPredicts() {
			var dbName = mw.config.get( "wgDBname" );
			var revID = mw.config.get( "wgCurRevisionId" );
			
			$.getJSON( "//ores.wikimedia.org/v3/scores/" + dbName + "/" + revID + "/drafttopic" )
				.done( function ( data ) {
					if(data && data[dbName] && data[dbName].scores &&
						data[dbName].scores[revID] &&
						data[dbName].scores[revID].drafttopic &&
						data[dbName].scores[revID].drafttopic.score &&
						data[dbName].scores[revID].drafttopic.score.prediction) {
						
						var prediction = data[dbName].scores[revID].drafttopic.score.prediction;
						console.log("Got ORES response! Raw predictions:");
						console.log(prediction);
						
						prediction.forEach( function (item) {
							var last = item.split(".")[item.split(".").length-1];
							var penultimate = item.split(".")[item.split(".").length-2];
							if ( last.substr(-1) == "*" ) {
								// Filter out redundant starred predictions
								if (prediction.find(element => (
									element.split(".")[element.split(".").length-1] != last &&
									element.split(".")[element.split(".").length-2] == penultimate
								) ) ) {
									console.log("Prediction \"" + last + "\" excluded.");
									last = null;
								} else {
									last = penultimate;
								}
							}
							
							if ( wikiprojects[last] ) {
								// WikiProject found, no need to try splitting
								predicts.push(last);
							} else if ( last ) {
								// Can't find wikiProject, try splitting
								var splitLast = last.split(/( & | and )/);
								for (i=0;i<=splitLast.length;i+=2) {
									splitLast[i] = splitLast[i].charAt(0).toUpperCase()
										+ splitLast[i].slice(1);
									predicts.push( splitLast[i] );
								}
							}
							
						} );
						console.log("Filtered predictions:");
						console.log(predicts);
						showPredicts();
					} else {
						console.error("Error finding predictions in ORES response:");
						console.error(data);
					}
				} ).fail( function ( error ) { 
				console.error("Error retrieving ORES data: " + error); 
			} );
			return;
		}

		function getTemplateCategories(cont) { // Recursively call API
			var query = {
				action: "query",
				list: "categorymembers",
				cmtitle: wikiProjectMainCategory,
				cmtype: "subcat",
				cmlimit: "max"
			};
			if (cont) {
				query = Object.assign(query, cont);
			}
			new mw.Api().get( query )
				.done (function (data) {
					if (data && data.query && data.query.categorymembers) { //API query returned members
						Object.entries(data.query.categorymembers).forEach( function(item) {
							wikiProjectCategories.push(item[1].title);
						} );
					}
					if (data && data.continue) { //More results are available
						getTemplateCategories(data.continue);
					} else {
						fetchJSONList(templateBlocklistName).then( (templateBlocklist) => {
							console.log("Template blocklist:");
							console.log(templateBlocklist);
							getTemplatesFromCategories(templateBlocklist);
						} );
					}
				} ).fail (function(error) {
					console.error("Error getting list of categories:");
					console.error(error);
				} );
			return;
		}
		
		function getTemplatesFromCategories(templateBlockList, catTitle, cont) { // Recursively call API
			if (typeof catTitle === 'undefined') { // Grab next item in wikiProjectCategories
				catTitle = wikiProjectCategories.pop();
			}
			if (typeof catTitle === 'undefined') { // No remaining entries in wikiProjectCategories
				// Manually add Wikiprojects with /s in their title
				fetchJSONList(templateIncludelistName).then( (templateIncludelist) => {
					console.log("Template includelist:");
					console.log(templateIncludelist);
					jQuery.extend( wikiprojects, templateIncludelist );
					writeTemplateCache();
				} );
			} else {
				var query = {
					action: "query",
					list: "categorymembers",
					cmtitle: catTitle,
					cmtype: "page",
					cmnamespace: "10",
					cmlimit: "max"
				};
				if (cont) {
					query = Object.assign(query, cont);
				}
				new mw.Api().get( query )
					.done (function (data) {
						if (data && data.query && data.query.categorymembers) { //API query returned members
							Object.entries(data.query.categorymembers).forEach( function(item) {
								var title = item[1].title.match(/^Template:WikiProject\s(.*)$/);
								if (title && title[1] && title[1] !== "") { //Valid page name format
									if (title[1].indexOf("/") == -1 || title[1].match(/ task ?force$/i) ) { //No subpages
										title[1] = title[1].replace(/ task ?force$/i,"");
										if( !(templateBlockList[ title[1] ]) ) { //Not on blocklist
											wikiprojects[ title[1] ] = item[1].title.replace("Template:", "");
										}
									}
								}
							} );
						}
						if (data && data.continue) { //More results are available
							getTemplatesFromCategories(templateBlockList, catTitle, data.continue);
						} else {
							getTemplatesFromCategories(templateBlockList);
						}
					} ).fail (function(error) {
						console.error("Error getting list of templates:");
						console.error(error);
						constructForm();
					} );
				return;
				
			}
		}

		// Construct the form
		function constructForm() {
			mw.loader.load( "oojs-ui.styles.icons-movement"); 
			
			Object.keys(wikiprojects).sort().forEach( function(name) {
				select.append( $( "<option>" )
					.attr( "value", wikiprojects[name] )
					.text( name ) );
			} );
			form.hide();
			form.empty();
			form.append( $( "<span>" )
				.text( "Tag WikiProjects: " )
				.css( {
					"font-size": "115%",
					"font-weight": "bold"
				} )
			);
			form.append( select );
			form.append( "&#32;&#32;" );
			form.append( submitButton.$element );
			form.append( cancelButton.$element );
			form.append( nextButton.$element );
			form.append ( $( "<ul>" )
				.attr( "id", "draft-sorter-status" )
			);
			form.show();
			$( select )
				.val( existingProjects )
				.chosen( {"placeholder_text_multiple": "Select some WikiProjects"} )
				.on("change", function(evt, params) { //Make existing projects undeletable
					$( "#draft-sorter-status" ).empty();
					if ( predicts.length > 0 ) { showPredicts(); }
					if ( params.deselected && existingProjects.includes(params.deselected) ) {
						$( select ).val( $( select ).val().concat([params.deselected]) ).trigger("chosen:updated");
						$( "#draft-sorter-status" ).prepend( $( "<li>" )
							.text( "Draft Sorter cannot remove existing WikiProjects." )
							.addClass( "error" )
						);
					}
				} );

			// Add completed form to the page
			$( '#draft-sorter-wrapper' ).replaceWith(form);
			getPredicts();
			return;
		}

		// The submission function
		function submit() {
			$( "#draft-sorter-form" )
				.attr("disabled", true)
				.trigger("chosen:updated");
			submitButton
				.setLabel( "Submitting..." )
				.setDisabled( true );
			cancelButton
				.setLabel ( "Close" );
				
			var newTags = [];

			$( "#draft-sorter-form" ).val().forEach( function (element) {
				if ( !existingProjects.includes(element) ) {
					newTags.push(element);
				}
			} );

			console.log( newTags.length + " new tag(s): " + newTags.join(", ") );
			var statusList = $( "#draft-sorter-status" )
				.html( "<li>Saving " + newTags.length + " new tags.</li>" );
			var showStatus = function ( status ) {
				return $( "<li>" )
					.text( status )
					.appendTo( statusList );
			};
			var newText = "";
			newTags.forEach( function ( element ) {
					newText += "{{" + element + "|importance=|class=draft}}\n";
			} );

			function editTalk(text, prefix) {
				var params = {
					action: "edit",
					title: "Draft talk:" + mw.config.get( "wgTitle" ),
					summary: "Tagging draft: +" + newTags.join(", +") +
						" ([[User:Ahecht/Scripts/draft-sorter.js|draft-sorter.js]])",
				};
				params[prefix + "text"] = text;

				new mw.Api().postWithEditToken( params ).done( function ( data ) {
					if ( data && data.edit && data.edit.result && data.edit.result === "Success" ) {
						showStatus( "Edit saved successfully! (" )
							.append( $( "<a>" )
								.text( "reload" )
								.attr( "href", "#" )
								.click( function () {
									window.location.replace( 
										window.location.href.replace("draftsorttrigger=y","")
									);
								} )
							).append( ")" );
						submitButton.setLabel( "Submitted" );
						nextButton.setLabel( "Next draft" ).setFlags( [ 'progressive' ] );
					} else {
						showStatus( "Couldn't save due to error: " + JSON.stringify( data ) );
					}
				} ).fail( function ( error ) {
					showStatus( "Couldn't save due to error: " + JSON.stringify( error ) );
				} );
				return;
			}

			new mw.Api().get( {
				action: "query",
				titles: "Draft talk:" + mw.config.get( 'wgTitle' ),
				prop: "templates",
				tltemplates: "Template:WikiProject_banner_shell"
			} ).done (function (data) {
				var bannerShellUsed = Object.entries(data.query.pages)[0][1].templates;
				if(typeof(bannerShellUsed) == "object" && bannerShellUsed.length > 0) {
					api.get( {
						action: "parse",
						page: "Draft talk:" + mw.config.get( 'wgTitle' ),
						prop: "wikitext",
						section: "0"
					} ).done (function (data) {
						var talkText = data.parse.wikitext["*"];
						if (typeof(talkText) == "string") {
							var pattern = /(\{\{\s*(?:Wiki[ _]?Project[ _]?banners?[ _]?shell(?:\/redirect)?|(?:(?:WP)?[ _]?Banner|(?:Wiki)?Project|Scope)[ _]?shell|Multiple[ _]wikiprojects|WikiProject[ _]?Banners?|WPBS?)\s*\|\s*)/im;
							if (talkText.search(pattern) >= 0) {
								newText = talkText.replace( pattern, ("$1" + newText) );
								editTalk(newText,"");
							} else {
								console.log("Banner shell on talk page, but not found in wikitext: " + talkText);
								editTalk(newText,"prepend");
							}
						} else {
							console.log("typeof(talkText) = " + typeof(talkText));
							editTalk(newText,"prepend");
						}
					} ).fail (function (error) {
						console.warn( "Couldn't retrieve talk page text due to error: " + JSON.stringify( error ) );
						editTalk(newText,"prepend");
					} );
				} else if(newTags.length > 2) {
					console.log("typeof(bannerShellUsed) = " + typeof(bannerShellUsed) );
					newText = "{{WikiProject banner shell|\n" + newText + "}}";
					editTalk(newText,"prepend");
				} else {
					console.log("typeof(bannerShellUsed) = " + typeof(bannerShellUsed) + "; newTags.length = " + newTags.length);
					editTalk(newText,"prepend");
				}
			} ).fail( function ( error ) {
				console.warn( "Couldn't retrieve templates on talk page due to error: " + JSON.stringify( error ) );
				editTalk(newText,"prepend");
			} );
			return;
		}
	} );
	if (mw.util.getParamValue('draftsorttrigger')) {
		$( portletLink ).trigger("click");
	}
} ) }( jQuery, mediaWiki ) );
//</nowiki>