User:SD0001/AFC-submit-wizard2.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:SD0001/AFC-submit-wizard2. |
/**
* 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> */