User:SD0001/AFC-submit-wizard2.js

Source: Wikipedia, the free encyclopedia.
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;
	}

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

	// Used to constuct two different API objects for the controller and evaluator, so that aborts on the
	// controller API don't stop the final evaluate process
	var apiOptions = {
		parameters: {
			format: 'json',
			formatversion: '2'
		},
		ajax: {
			headers: {
				'Api-User-Agent': 'w:en:MediaWiki:AFC-submit-wizard.js'
			}
		}
	};

	function View() {
		this.constructFieldset();
		this.highlightSubmitter();
		this.loadMainCategoryOptions();
		this.loadWikiProjectCategoryOptions();
		this.getInfoboxWikiProjectMap();
		this.attchHandlers();
	}
	View.prototype.constructFieldset = function() {
		// Create the UI
		this.fieldset = new OO.ui.FieldsetLayout({
			label: 'Submit your draft for review at Articles for Creation (AfC)',
			classes: ['container'],
			items: [
				this.draftLayout = new OO.ui.FieldLayout(this.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
				}),

				this.rawClassLayout = new OO.ui.FieldLayout(this.rawClass = new OO.ui.RadioSelectInputWidget(), {
					label: 'Choose the most appropriate category',
					help: 'For biographies about scholars, choose one of the two biography categories rather than one associated to their field',
					align: 'inline'
				}),

				this.shortdescLayout = new OO.ui.FieldLayout(this.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
				}),

				this.talkTagsLayout = new OO.ui.FieldLayout(this.talkTagsInput = new OO.ui.MenuTagMultiselectWidget({
					placeholder: 'Start typing to search for tags ...',
					tagLimit: 10,
					autocomplete: false,
					// $overlay: $('body')[0]
				}), {
					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
				this.topicsLayout = new OO.ui.FieldLayout(this.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
				}),

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

			]
		});

		this.topicsLayout.toggle(false);
		$('.mw-ui-button').parent().replaceWith(this.fieldset.$element);
	};
	View.prototype.getJSONPage = function (page) {
		// Load a JSON page from the wiki
		return $.getJSON('https://en.wikipedia.org/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=text/json');
	};
	View.prototype.loadMainCategoryOptions = function() {
		this.topicOptionsLoaded = $.Deferred();
		this.getJSONPage('Wikipedia:WikiProject Articles for creation/AFC topic map.json').then(function(optionsJson) {
			var options = $.map(optionsJson, function(info, code) {
				return {
					label: info.label,
					data: code
				};
			});
			this.rawClass.setOptions(options);
			this.rawClass.setValue('o'); // default: other

			// put allowed option codes in promise resolution:
			this.topicOptionsLoaded.resolve(options.map(function(op) {
				return op.data;
			}));
		}.bind(this));
	};
	View.prototype.loadWikiProjectCategoryOptions = function() {
		// populate talk page tags for multi-select widget
		this.talkTagOptionsLoaded = $.Deferred();
		this.getJSONPage('Wikipedia:WikiProject Articles for creation/WikiProject templates.json').then(function (data) {
			this.talkTagsInput.addOptions(Object.keys(data).map(function (k) {
				return {
					data: data[k],
					label: k
				};
			}));
			this.talkTagOptionsLoaded.resolve();
		}.bind(this));
	};
	View.prototype.setTalkTags = function(tags) {
		this.talkTagOptionsLoaded.then(function () {
			this.talkTagsInput.setValue(tags);
		});
	};
	View.prototype.getInfoboxWikiProjectMap = function() {
		// Get mapping of infoboxes with relevant WikiProjects
		this.ibxmapLoaded = $.Deferred();
		this.getJSONPage('Wikipedia:WikiProject Articles for creation/Infobox WikiProject map.json').then(function (data) {
			this.ibxmapLoaded.resolve(data);
		}.bind(this));
	};
	View.prototype.highlightSubmitter = function() {
		var asUser = mw.util.getParamValue('username');
		if (asUser && asUser !== mw.config.get('wgUserName')) {
			this.fieldset.addItems([
				new OO.ui.FieldLayout(new OO.ui.MessageWidget({
					type: 'notice',
					inline: true,
					label: 'Submitting as User:' + asUser
				}))
			], /* position */ 6); // just before submit button
		}
	};
	View.prototype.attchHandlers = function() {
		var formController = new Controller();

		this.draftInput.on('change', formController.onInputChange);
		if (mw.util.getParamValue('draft')) {
			formController.onInputChange();
		}

		this.submitButton.$element.on('click', function() {
			new Evaluate();
		});
	};
	View.prototype.setStatus = function(type, message) {
		if (!this.statusArea) {
			this.fieldset.addItems([
				this.statusLayout = new OO.ui.FieldLayout(this.statusArea = new OO.ui.MessageWidget())
			]);
		}
		this.statusArea.setType(type);
		this.statusArea.setLabel(message);
	};
	View.prototype.removeStatusArea = function() {
		this.fieldset.removeItems([this.statusLayout]);
	};
	View.prototype.setTalkStatus = function(type, message) {
		if (!this.talkStatusArea) {
			this.fieldset.addItems([
				new OO.ui.FieldLayout(this.talkStatusArea = new OO.ui.MessageWidget())
			]);
		}
		this.talkStatusArea.setType(type);
		this.talkStatusArea.setLabel(message);
	};



	function Controller() { // Singleton class
		if (Controller.instance) {
			return Controller.instance;
		}
		Controller.instance = this;
		this.api = new mw.Api(apiOptions);
	}
	Controller.prototype.resetVariables = function() {
		this.oresTopics = null;
		this.talktext = null;
		this.pagetext = null;
		view.setTalkTags([]);
	};
	Controller.prototype.onInputChange = function() {
		this.api.abort(); // abort any earlier API requests
		this.title = view.draftInput.getValue().trim();
		if (!this.title) {
			return;
		}
		this.resetVariables();
		this.fetchPageData().then(function() {
			this.setPrefillsFromPageData();
		});
		this.getTalkPageData().then(function() {
			this.setPrefillsFromTalkPageData();
		});
	};
	Controller.prototype.fetchPageData = function() {
		return this.api.get({
			"action": "query",
			"prop": "revisions|description|info",
			"titles": this.title,
			"rvprop": "content",
			"rvslots": "main"
		}).then(function(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 (view.draftInput.getValue() !== (preNormalizedTitle || page.title)) {
				return $.Deferred().reject(); // user must have changed the title already
			}
			if (!page || page.invalid) {
				view.draftLayout.setErrors(['Please check draft title. This title is invalid.']);
				return $.Deferred().reject();
			}
			if (page.missing) {
				view.draftLayout.setErrors(['Please check draft title. No such draft exists.']);
				return $.Deferred().reject();
			}
			this.pagetext = page.revisions[0].slots.main.content;
			this.shortdesc = page.description;
			this.lastrevid = page.lastrevid;

			// Show no refs warning
			if (!/<ref>/.test(this.pagetext) && !/\{\{[Ss]fn\}\}/.test(this.pagetext)) {
				view.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>.')
				]);
			}
		}.bind(this));
	};
	Controller.prototype.setPrefillsFromPageData = function() {

		// set main category
		var topicMatch = this.pagetext.match(/\{\{AFC topic\|(.*?)\}\}/);
		if (topicMatch) {
			view.topicOptionsLoaded.then(function(allowedCodes) {
				var topic = topicMatch[1];
				console.log(topic);
				console.log(allowedCodes);
				// if the code found in the template is an invalid one, keep the default to "other",
				// rather than the first item in the list
				if (allowedCodes.indexOf(topic) !== -1) {
					view.rawClass.setValue(topic);
				} else {
					view.rawClass.setValue('o');
				}
			});
		} else {
			view.rawClass.setValue('o');
		}

		// set shortdesc in form
		view.shortdescInput.setValue(this.shortdesc || '');

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

		// fill ORES topics
		this.getOresTopics(this.lastrevid).then(function (topics) {
			console.log('ORES topics: ', topics);
			if (!topics || !topics.length) { // unexpected API response or API returns unsorted
				view.topicsLayout.toggle(true);
			} else {
				view.topicsLayout.toggle(false);
				this.oresTopics = topics;
			}
		}.bind(this), function () {
			view.topicsLayout.toggle(true);
		});
	};
	Controller.prototype.getTalkPageData = function() {
		this.titleObj = mw.Title.newFromText(this.title);
		if (!this.titleObj || this.titleObj.isTalkPage()) {
			return;
		}
		var talkpagename = this.titleObj.getTalkPage().toText();
		console.log(talkpagename);
		return this.api.get({
			"action": "query",
			"prop": "revisions",
			"titles": talkpagename,
			"rvprop": "content",
			"rvslots": "main",
			"tllimit": "max"
		}).then(function(json) {
			var talkpage = json.query.pages[0];
			if (!talkpage || talkpage.missing) {
				return;
			}
			this.talktext = talkpage.revisions[0].slots.main.content;
			console.log(this.talktext);
		}.bind(this));
	};
	Controller.prototype.setPrefillsFromTalkPageData = function() {
		var existingWikiProjects = this.extractWikiProjectTagsFromText(this.talktext);
		var existingTags = existingWikiProjects.map(function (e) {
			return e.name;
		});
		view.talkTagOptionsLoaded.then(function () {
			console.log('setValue2:', view.talkTagsInput.getValue().concat(existingTags));
			view.talkTagsInput.setValue(view.talkTagsInput.getValue().concat(existingTags));
		});
		console.log(existingTags);
	};
	Controller.prototype.getOresTopics = function(revid) {
		return $.get('https://ores.wikimedia.org/v3/scores/enwiki/?models=drafttopic&revids=' + revid).then(function (json) {

			// undefined 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');
				});
		});
	};
	Controller.prototype.extractWikiProjectTagsFromText = function(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() {
		this.api = new mw.Api(apiOptions);
		this.attachStatusArea();
		this.fetchPageData().then(function() {
			var text = this.prepareDraftPageText();
			this.saveDraftPage(text);

			view.setTalkStatus('notice', 'Saving draft talk page ...');
			// we already fetched the talk page, don't do it again
			var talktext = this.prepareTalkPageText(new Controller().talktext);
			this.saveTalkPage(talktext);
		}.bind(this));
	}
	Evaluate.prototype.fetchPageData = function() {
		this.draft = view.draftInput.getValue().trim();

		return this.api.get({
			"action": "query",
			"prop": "revisions",
			"titles": this.draft,
			"rvprop": "content",
			"rvslots": "main"
		}).then(function(json) {
			var page = json.query.pages[0];
			if (!page || page.invalid || page.missing) {
				view.draftLayout.setErrors(['Please check draft title. No such draft exists.']);
				view.removeStatusArea();
				return $.Deferred().reject();
			}
			this.text = page.revisions[0].slots.main.content;
		}.bind(this)).catch(function (err) {
			view.setStatus('error', 'An error occurred (' + err + '). Please try again or refer to the help desk.');
			return $.Deferred().reject();
		}.bind(this));
	};
	Evaluate.prototype.prepareDraftPageText = function() {
		var header = '';

		var controller = new Controller(); // get controller instance

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

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

		// main category
		this.text = this.text.replace(/\{\{AFC topic\|(.*?)\}\}/g, '');
		header += '{{AFC topic|' + view.rawClass.getValue() + '}}\n';

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

		// insert everything to the top
		return header + this.text;

	};
	Evaluate.prototype.saveDraftPage = function(text) {
		view.setStatus('notice', 'Processing ...');

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

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

	Evaluate.prototype.prepareTalkPageText = function(talktext) {
		var alreadyExistingWikiProjects = new Controller().extractWikiProjectTagsFromText();
		var alreadyExistingTags = alreadyExistingWikiProjects.map(function (e) {
			return e.name;
		});
		var tagsToAdd = view.talkTagsInput.getValue().filter(function (tag) {
			return alreadyExistingTags.indexOf(tag) === -1;
		});
		var tagsToRemove = alreadyExistingTags.filter(function (tag) {
			return view.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' : '');

		return tagsToAddText + (talktext || '');
	};
	Evaluate.prototype.saveTalkPage = function(talktext) {
		this.api.postWithEditToken({
			"action": "edit",
			"title": new mw.Title(this.draft).getTalkPage().toText(),
			"text": talktext,
			"summary": 'Adding WikiProject tags using [[MediaWiki:AFC-submit-wizard.js|AFC-submit-wizard]])' // TODO: create documentation page in WP space and link to that instead
		}).then(function (data) {
			if (data.edit && data.edit.result === 'Success') {
				view.setTalkStatus('success', 'Successfully added WikiProject tags to talk page');
			} else {
				return $.Deferred().reject('unexpected-result');
			}
		}).catch(function (err) {
			view.setTalkStatus('error', 'An error occurred in editing the talk page (' + err + ').');
		});
	};


	// Start the script
	var view = new View();


});

/* </nowiki> */