User:SD0001/AFC-submit-wizard2.js

/** * MediaWiki:AFC-submit-wizard.js * * JavaScript used for submitting drafts to AfC. * Used on Articles for creation/Submitting. * Loaded via Snippets/Load JS and CSS by URL. * * Author: User:SD0001 * Licence: MIT */

/* jshint maxerr: 999 */ /* globals mw, $, OO */ /* */

$.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 (!/ /.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 help on adding references.')				]); }		}.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 += '\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 += '\n'; }

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

// put AFC submission template header += '' + (mw.util.getParamValue('username')\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 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 ;		}).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 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;

});

/* */