MediaWiki:AFC-submit-wizard.js

This is an old revision of this page, as edited by SD0001 (talk | contribs) at 05:47, 4 October 2020 (fix). 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.
/**
 * MediaWiki:AFC-submit-wizard.js
 *
 * JavaScript used for submitting drafts to AfC.
 * Used on [[Wikipedia:Articles for creation/Submitting]].
 * Loaded via [[mw:Snippets/Load JS and CSS by URL]].
 *
 * Author: [[User:SD0001]]
 * Licence: MIT
 */

/* jshint maxerr: 999 */
/* globals mw, $, OO */
/* <nowiki> */

$.when(
	$.ready,
	mw.loader.using([
		'mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 
		'mediawiki.widgets', 'oojs-ui-core', 'oojs-ui-widgets'
	])
).then(function () {

	if (mw.config.get('wgPageName') !== 'Wikipedia:Articles_for_creation/Submitting') {
		return;
	}
	
	if (mw.util.getParamValue('withJS') !== 'MediaWiki:AFC-submit-wizard.js') {
		return;
	}


	$('#firstHeading').text('Submitting your draft ...');
	document.title = 'Submitting your draft ...';

	var apiOptions = {
		parameters: {
			format: 'json',
			formatversion: '2'
		},
		ajax: {
			headers: {
				'Api-User-Agent': 'w:en:MediaWiki:AFC-submit-wizard.js'
			}
		}
	};

	// Two different API objects so that aborts on the lookupApi don't stop the final
	// evaluate process
	var lookupApi = new mw.Api(apiOptions),
		submitApi = new mw.Api(apiOptions);

	var draftLayout, talkTagsLayout, shortdescLayout, isBLPLayout, isCompanyLayout, topicsLayout, submitLayout, draftInput, talkTagsInput, shortdescInput, isBLPInput, isCompanyInput, topicsInput, submitButton, mainStatusLayout, mainStatusArea, talkStatusLayout, talkStatusArea;

	// global
	var pagetext, talktext, oresTopics;

	// Create the UI
	var fieldset = new OO.ui.FieldsetLayout({
		label: 'Submit your draft for review at Articles for Creation (AfC)',
		classes: ['container'],
		items: [
			draftLayout = new OO.ui.FieldLayout(draftInput = new mw.widgets.TitleInputWidget({
				value: (mw.util.getParamValue('draft') || '').replace(/_/g, ' '),
				placeholder: 'Enter the draft title, begins with "Draft:" or "User:"'
			}), {
				label: 'Draft title',
				align: 'top',
				help: 'This should be pre-filled if you clicked the link while on the draft page',
				helpInline: true
			}),

			isBLPLayout = new OO.ui.FieldLayout(isBLPInput = new OO.ui.CheckboxInputWidget({
				selected: false
			}), {
				label: 'This draft is a biography of a living person',
				align: 'inline'
			}),

			isCompanyLayout = new OO.ui.FieldLayout(isCompanyInput = new OO.ui.CheckboxInputWidget({
				selected: false
			}), {
				label: 'This draft is about a company or a corporation',
				align: 'inline'
			}),

			shortdescLayout = new OO.ui.FieldLayout(shortdescInput = new OO.ui.TextInputWidget({
				placeholder: 'Briefly describe the subject (eg. "Kenyan astronomer", "Indian dessert")',
				maxLength: 100
			}), {
				label: 'Short description',
				align: 'top',
				help: 'Try not to exceed 40 characters',
				helpInline: true
			}),

			talkTagsLayout = new OO.ui.FieldLayout(talkTagsInput = new OO.ui.MenuTagMultiselectWidget({
				placeholder: 'Start typing to search for tags ...',
				tagLimit: 10,
				autocomplete: false,
				$overlay: $('<div>').addClass('projectTagOverlay').css({
					'position': 'absolute',
					'z-index': '110'
				}).appendTo('body')
			}), {
				label: 'WikiProject classification tags',
				align: 'top',
				help: 'Adding the 1–4 most applicable WikiProjects is plenty. For example, if you add the Physics tag, you do not need to also add the Science tag.',
				helpInline: true
			}),

			// This is shown only if the ORES topic lookup fails, or is inconclusive
			topicsLayout = new OO.ui.FieldLayout(topicsInput = new OO.ui.MenuTagMultiselectWidget({
				placeholder: 'Start typing to search for topics ...',
				tagLimit: 10,
				autocomplete: false, // XXX: doesn't seem to work
				options: ["biography", "women", "food-and-drink", "internet-culture", "linguistics", "literature", "books", "entertainment", "films", "media", "music", "radio", "software", "television", "video-games", "performing-arts", "philosophy-and-religion", "sports", "architecture", "comics-and-anime", "fashion", "visual-arts", "geographical", "africa", "central-africa", "eastern-africa", "northern-africa", "southern-africa", "western-africa", "central-america", "north-america", "south-america", "asia", "central-asia", "east-asia", "north-asia", "south-asia", "southeast-asia", "west-asia", "eastern-europe", "europe", "northern-europe", "southern-europe", "western-europe", "oceania", "business-and-economics", "education", "history", "military-and-warfare", "politics-and-government", "society", "transportation", "biology", "chemistry", "computing", "earth-and-environment", "engineering", "libraries-and-information", "mathematics", "medicine-and-health", "physics", "stem", "space", "technology"].map(function (e) {
					return {
						data: e,
						label: e
					};
				})
			}), {
				label: 'Topic classifiers',
				align: 'top',
				help: 'Pick the topic areas that are relevant',
				helpInline: true
			}),

			submitLayout = new OO.ui.FieldLayout(submitButton = new OO.ui.ButtonWidget({
				label: 'Submit',
				flags: ['progressive', 'primary'],
			})),

		]
	});

	topicsLayout.toggle(false);

	var asUser = mw.util.getParamValue('username');
	if (asUser && asUser !== mw.config.get('wgUserName')) {
		fieldset.addItems([
			new OO.ui.FieldLayout(new OO.ui.MessageWidget({
				type: 'notice',
				inline: true,
				label: 'Submitting as User:' + asUser
			}))
		], /* position */ 6);
	}

	$('.mw-ui-button').parent().replaceWith(fieldset.$element);

	// Load a JSON page from the wiki
	function getJSONPage(page) {
		return $.getJSON('https://en.wikipedia.org/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=text/json');
	}

	// populate talk page tags for multi-select widget
	var talkTagOptionsLoaded = $.Deferred();
	getJSONPage('Wikipedia:WikiProject Articles for creation/WikiProject templates.json').then(function (data) {
		talkTagsInput.addOptions(Object.keys(data).map(function (k) {
			return {
				data: data[k],
				label: k
			};
		}));
		talkTagOptionsLoaded.resolve();
	});

	// Get mapping of infoboxes with relevant WikiProjects
	var ibxmapLoaded = $.Deferred();
	getJSONPage('Wikipedia:WikiProject Articles for creation/Infobox WikiProject map.json').then(function (data) {
		ibxmapLoaded.resolve(data);
	});

	submitButton.$element.on('click', evaluate);
	draftInput.on('change', onDraftInputChange);

	if (mw.util.getParamValue('draft')) {
		onDraftInputChange();
	}

	// The default font size in monobook and modern are too small at 10px
	mw.util.addCSS('.skin-modern .projectTagOverlay, .skin-monobook .projectTagOverlay { font-size: 130%; }');

	function onDraftInputChange() {
		lookupApi.abort(); // abort older API requests

		var drafttitle = draftInput.getValue().trim();
		if (!drafttitle) { // empty
			return;
		}

		console.log('draft input changed: "' + draftInput.getValue() + '"');

		// re-initialize
		draftLayout.setErrors([]);
		draftLayout.setWarnings([]);
		oresTopics = null;
		talktext = null;
		pagetext = null;

		talkTagOptionsLoaded.then(function () {
			talkTagsInput.setValue([]);
		});

		lookupApi.get({
			"action": "query",
			"prop": "revisions|description|info",
			"titles": drafttitle,
			"rvprop": "content",
			"rvslots": "main"
		}).then(function (json) {
			console.log(json);
			var page = json.query.pages[0];
			var preNormalizedTitle = json.query.normalized && json.query.normalized[0] &&
				json.query.normalized[0].from;
			console.log('page.title: "' + page.title + '"');
			if (draftInput.getValue() !== (preNormalizedTitle || page.title)) {
				return; // user must have changed the title already
			}
			if (!page || page.invalid) {
				draftLayout.setErrors(['Please check draft title. This title is invalid.']);
				return;
			}
			if (page.missing) {
				draftLayout.setErrors(['Please check draft title. No such draft exists.']);
				return;
			}
			pagetext = page.revisions[0].slots.main.content;

			// Show no refs warning
			if (!/<ref>/.test(pagetext) && !/\{\{[Ss]fn\}\}/.test(pagetext)) {
				draftLayout.setWarnings([
					new OO.ui.HtmlSnippet('This draft doesn\'t appear to contain any references. Please add references, without this it will almost certainly be declined. See <a href="/wiki/Help:Introduction_to_referencing_with_Wiki_Markup/2" target="_blank">help on adding references</a>.')
				]);
			}

			// set checkbox states
			isBLPInput.setSelected(/\{\{[Dd]raftBLP\}\}/.test(pagetext));
			isCompanyInput.setSelected(/\{\{[Dd]raftCorp\}\}/.test(pagetext));

			// set shortdesc in form
			shortdescInput.setValue(page.description || '');

			// guess wikiproject tags from infoboxes on the page
			$.when(ibxmapLoaded, talkTagOptionsLoaded).then(function (ibxmap) {
				var infoboxRgx = /\{\{([Ii]nfobox [^|}]*)/g,
					wikiprojects = [],
					match;
				while (match = infoboxRgx.exec(pagetext)) {
					var ibx = match[1].trim();
					if (ibxmap[ibx]) {
						wikiprojects = wikiprojects.concat(ibxmap[ibx]);
					}
				}
				console.log('wikiprojects from infobox: ', wikiprojects);
				console.log('setValue1:', talkTagsInput.getValue().concat(wikiprojects));
				talkTagsInput.setValue(talkTagsInput.getValue().concat(wikiprojects));
			});

			// fill ORES topics
			getOresTopics(page.lastrevid).then(function (topics) {
				console.log('ORES topics: ', topics);
				if (!topics || !topics.length) { // unexpected API response or API returns unsorted
					topicsLayout.toggle(true);
				} else {
					topicsLayout.toggle(false);
					oresTopics = topics;
				}
			}, function () {
				topicsLayout.toggle(true);
			});

		});


		var titleObj = mw.Title.newFromText(drafttitle);
		if (!titleObj || titleObj.isTalkPage()) {
			return;
		}
		var talkpagename = titleObj.getTalkPage().toText();
		console.log(talkpagename);
		lookupApi.get({
			"action": "query",
			"prop": "revisions",
			"titles": talkpagename,
			"rvprop": "content",
			"rvslots": "main",
		}).then(function (json) {
			var talkpage = json.query.pages[0];
			if (!talkpage || talkpage.missing) {
				return;
			}
			talktext = talkpage.revisions[0].slots.main.content;
			console.log(talktext);

			var existingWikiProjects = extractWikiProjectTagsFromText(talktext);
			var existingTags = existingWikiProjects.map(function (e) {
				return e.name;
			});
			talkTagOptionsLoaded.then(function () {
				console.log('setValue2:', talkTagsInput.getValue().concat(existingTags));
				talkTagsInput.setValue(talkTagsInput.getValue().concat(existingTags));
			});

			console.log(existingTags);

		});

	}

	function getOresTopics(revid) {
		return $.get('https://ores.wikimedia.org/v3/scores/enwiki/?models=drafttopic&revids=' + revid).then(function (json) {

			// null is returned if at any point something in the API output is unexpected
			// ES2020 has optional chaining, but of course on MediaWiki we're still stuck with ES5
			return json &&
				json.enwiki &&
				json.enwiki.scores &&
				json.enwiki.scores[revid] &&
				json.enwiki.scores[revid].drafttopic &&
				json.enwiki.scores[revid].drafttopic.score &&
				(json.enwiki.scores[revid].drafttopic.score.prediction instanceof Array) &&
				json.enwiki.scores[revid].drafttopic.score.prediction.map(function (topic, idx, topics) {
					// Remove Asia.Asia* if Asia.South-Asia is present (example)
					if (topic.slice(-1) === '*') {
						var metatopic = topic.split('.').slice(0, -1).join('.');
						for (var i = 0; i < topics.length; i++) {
							if (topics[i] !== topic && topics[i].startsWith(metatopic)) {
								return;
							}
						}
						return metatopic.split('.').pop();
					}
					return topic.split('.').pop();
				})
				.filter(function (e) {
					return e; // filter out undefined from above
				})
				.map(function (topic) {
					// convert topic string to normalised form
					return topic
						.replace(/[A-Z]/g, function (match) {
							return match[0].toLowerCase();
						})
						.replace(/ /g, '-')
						.replace(/&/g, 'and');
				});
		});
	}

	function extractWikiProjectTagsFromText(text) {
		if (!text) {
			return [];
		}

		// this is best-effort, no guaranteed accuracy
		var existingTags = [];
		var rgx = /\{\{(WikiProject [^|}]*).*?\}\}/g;
		var match;
		while (match = rgx.exec(text)) { // jshint ignore:line
			var tag = match[1].trim();
			if (tag === 'WikiProject banner shell') {
				continue;
			}
			existingTags.push({
				wikitext: match[0],
				name: tag
			});
		}
		return existingTags;
	}

	function evaluate() {

		if (!mainStatusLayout || !mainStatusLayout.isElementAttached()) {
			fieldset.addItems([
				mainStatusLayout = new OO.ui.FieldLayout(mainStatusArea = new OO.ui.MessageWidget({
					type: 'notice',
					label: 'Processing ...'
				}))
			]);
		}

		var draft = draftInput.getValue();
		console.log(draft);
		console.log(isBLPInput.getValue());

		submitApi.get({
			"action": "query",
			"prop": "revisions",
			"titles": draft,
			"rvprop": "content",
			"rvslots": "main"
		}).then(function (json) {
			var page = json.query.pages[0];
			if (!page || page.invalid || page.missing) {
				draftLayout.setErrors(['Please check draft title. No such draft exists.']);
				fieldset.removeItems([mainStatusLayout]);
				return;
			}
			var text = page.revisions[0].slots.main.content;

			var header = '';

			// add shortdesc
			if (shortdescInput.getValue()) {
				text = text.replace(/\{\{[Ss]hort description\|.*?\}\}\n*/g, '');
				header += '{{Short description|' + shortdescInput.getValue() + '}}\n';
			}

			// draft topics
			console.log(topicsInput);

			if (topicsLayout.isVisible()) {
				oresTopics = topicsInput.getValue();
			}
			if (oresTopics.length) {
				text = text.replace(/\{\{[Dd]raft topics\|.*?\}\}\n*/g, '');
				header += '{{Draft topics|' + oresTopics.join('|') + '}}\n';
			}

			// put BLP/Company categories
			text = text.replace(/\{\{[Dd]raft(BLP|Corp)\}\}\n*/g, '');
			if (isBLPInput.isSelected()) {
				header += '{{DraftBLP}}\n';
			}
			if (isCompanyInput.isSelected()) {
				header += '{{DraftCorp}}\n';
			}

			// put AFC submission template 
			header += '{{subst:submit|' + (mw.util.getParamValue('username') || mw.config.get('wgUserName')) + '}}\n';

			// insert everything to the top
			text = header + text;

			console.log(text);

			mainStatusArea.setType('notice');
			mainStatusArea.setLabel('Saving draft page ...');

			// saving draft page
			submitApi.postWithEditToken({
				"action": "edit",
				"title": draft,
				"text": text,
				"summary": 'Submitting using [[MediaWiki:AFC-submit-wizard.js|AFC-submit-wizard]])'
			}).then(function (data) {
				if (data.edit && data.edit.result === 'Success') {
					mainStatusArea.setType('success');
					mainStatusArea.setLabel('Submission succeeded. Redirecting you to the draft page ...');

					setTimeout(function () {
						___location.href = mw.util.getUrl(draft);
					}, 1000);
				} else {
					return $.Deferred().reject('unexpected-result');
				}
			}).catch(function (err) {
				mainStatusArea.setType('error');
				mainStatusArea.setLabel('An error occurred (' + err + '). Please try again or refer to the help desk.');
			});

			fieldset.addItems([
				talkStatusLayout = new OO.ui.FieldLayout(talkStatusArea = new OO.ui.MessageWidget({
					type: 'notice',
					label: 'Saving draft talk page ...'
				}))
			]);

			// Process text of the talk page
			var alreadyExistingWikiProjects = extractWikiProjectTagsFromText(talktext);
			var alreadyExistingTags = alreadyExistingWikiProjects.map(function (e) {
				return e.name;
			});
			var tagsToAdd = talkTagsInput.getValue().filter(function (tag) {
				return alreadyExistingTags.indexOf(tag) === -1;
			});
			var tagsToRemove = alreadyExistingTags.filter(function (tag) {
				return talkTagsInput.getValue().indexOf(tag) === -1;
			});

			tagsToRemove.forEach(function (tag) {
				talktext = talktext.replace(new RegExp('\\{\\{\\s*' + tag + '\\s*(\\|.*?)?\\}\\}\\n?'), '');
			});

			var tagsToAddText = tagsToAdd.map(function (tag) {
				return '{{' + tag + '}}';
			}).join('\n') + (tagsToAdd.length ? '\n' : '');

			talktext = tagsToAddText + (talktext || '');

			submitApi.postWithEditToken({
				"action": "edit",
				"title": new mw.Title(draft).getTalkPage().toText(),
				"text": talktext,
				"summary": 'Adding WikiProject tags using [[MediaWiki:AFC-submit-wizard.js|AFC-submit-wizard]])'
			}).then(function (data) {
				if (data.edit && data.edit.result === 'Success') {
					talkStatusArea.setType('success');
					talkStatusArea.setLabel('Successfully added WikiProject tags to talk page');
				} else {
					return $.Deferred().reject('unexpected-result');
				}
			}).catch(function (err) {
				talkStatusArea.setType('error');
				talkStatusArea.setLabel('An error occurred in editing the talk page (' + err + ').');
			});

		}).catch(function (err) {
			mainStatusArea.setType('error');
			mainStatusArea.setLabel('An error occurred (' + err + '). Please try again or refer to the help desk.');
		});

	}

});

/* </nowiki> */