प्रयोगकर्ता:SM7/XFDcloser.js: रिवीजन सभ के बीचा में अंतर
Content deleted Content added
Maintenance: mw:RL/MGU - Updated deprecated module name |
updated version from enwiki |
||
लाइन 1:
/***************************************************************************************************
XFDcloser
- See [[User:Evad37/XFDcloser]] for installation, documentation, and important information
- Licencing and attribution:
https://en.wikipedia.org/wiki/User:Evad37/XFDcloser#Licencing_and_attribution
***************************************************************************************************/
/* jshint esversion: 5, laxbreak: true, undef: true, eqnull: true, maxerr: 3000 */
/* globals console, document, window, $, mw, importStylesheet, OO, extraJs, Morebits */
/* <nowiki> */
var XFDC_VERSION = "3.14.0";
// Record script start time ASAP
var XFDC_START_TIME = new Date();
/* ========== Dependencies ====================================================================== */
var getWikipageScript = function getWikipageScript(page) {
var pageUrl = 'https://en.wikipedia.org/w/index.php?title=' + page.replace(/ /g, '_') +
'&action=raw&ctype=text/javascript';
return mw.loader.getScript(pageUrl);
};
$.when(
// Resource loader modules
mw.loader.using([
'mediawiki.util', 'mediawiki.api', 'mediawiki.Title',
'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui'
]),
// Other scripts
window.extraJs || getWikipageScript('User:Evad37/extra.js'),
window.Morebits || getWikipageScript('MediaWiki:Gadget-morebits.js'),
// Stylesheets
importStylesheet( 'User:Evad37/XFDcloser/styles.css' ),
importStylesheet( 'MediaWiki:Gadget-morebits.css' ),
// Page ready
$.ready
).then(function() {
/* ========== Config ============================================================================ */
Line 65 ⟶ 44:
config.script = {
// Advert to append to edit summaries
advert: ' ([[WP:XFDC|
version:
};
// MediaWiki configuration values
Line 74 ⟶ 53:
'wgUserName',
'wgFormattedNamespaces',
'wgMinervaPermissions'
] );
/* --------- Quick checks that script should be running ----------------------------------------- */
if ( /(?:\?|&)(?:action|diff|oldid)=/.test(window.location.href) ) {
// Page is in edit, history, diff, or oldid mode
return;
}
if (
) {
// User is not extendedconfirmed or sysop
return;
}
var xfdpage_regex = /(हटावे_खातिर_लेख\/|
if ( !xfdpage_regex.test(config.mw.wgPageName) ) {
// Current page is not an XfD page;
return;
}
/* --------- Additional configuration ----------------------------------------------------------- */
config.isMobileSite = window.location.host.includes(".m.") || window.location.search.includes("useformat=mobile");
// Set custom version of namespaces with description for namespace 0
config.mw.namespaces = $.extend({}, config.mw.wgFormattedNamespaces, {0: 'article'});
// Month names - no longer provided by mw.config, see phab:T219340
config.wgMonthNames = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
// 0-indexed month names (remove "" from start of array)
config
// Set sysop status
if (
config.user = {
'isSysop': true,
Line 162 ⟶ 93:
config.user = {
'isSysop': false,
'sig': '<small>[[
};
}
// Start time, for detecting edit conflicts
config.startTime = XFDC_START_TIME;
// Variables for tracking across multiple discussions
config.track = {
Line 183 ⟶ 116:
});
// ========== Convinenance functions ============================================================ */
/**
* Un-escapes some HTML tags (<br>, <p>, <ul>, <li>, <hr>, <strong>, <em>, and <pre>);
* turns wikilinks into real links. Ignores anyting within <pre>...</pre> tags.
* Input will first be escaped using mw.html.escape() unless specified
* @param {String} text
* @param {Object} config Configuration options
* @config {Boolean} noEscape - do not escape the input first
* @returns {String} unescaped text
*/
var safeUnescape = function(text, config) {
return ( config && config.noEscape && text || mw.html.escape(text) )
// Step 1: unescape <pre> tags
.replace(
/<(\/?pre\s?\/?)>/g,
'<$1>'
)
// Step 2: replace piped wikilinks with real links (unless inside <pre> tags)
.replace(
/\[\[([^\|\]]*?)\|([^\|\]]*?)\]\](?![^<]*?<\/pre>)/g,
'<a href="' + mw.util.getUrl('$1') + '" target="_blank">$2</a>'
)
// Step 3: replace other wikilinks with real links (unless inside <pre> tags)
.replace(
/\[\[([^\|\]]+?)]\](?![^<]*?<\/pre>)/g,
'<a href="' + mw.util.getUrl('$1') + '" target="_blank">$1</a>'
)
// Step 4: unescape other tags (unless inside <pre> tags)
.replace(
/<(\/?(?:br|p|ul|li|hr|strong|em)\s?\/?)>(?![^<]*?<\/pre>)/g,
'<$1>'
);
};
var formatErrorMessage = function(details) {
var type = details && details.type || 'Script';
var error = details && details.error || 'Unknown error';
var message = ( details.message )
? ' - ' + safeUnescape(details.message)
: '';
return type + ' error: ' + error + message;
};
// TODO: how to pass in a message.... or maybe this should just return {type:..., error:...} objects?
var formatApiError = function(code, jqxhr, message) {
if ( code === 'http') {
var httpError = ( jqxhr.textStatus === 'error')
? jqxhr.xhr.status
: jqxhr.textStatus;
return formatErrorMessage({
type: 'HTTP',
error: httpError,
message: message
});
}
if ( code === 'okay-but-empty' ) {
return formatErrorMessage({
type: 'Server',
error: 'Got an empty response from the server'
});
}
return formatErrorMessage({
type: 'API',
error: code,
message: message
});
};
/* ========== XfdVenue class ====================================================================
Each instance represents an XfD venue, with properties/function specific to that venue
---------------------------------------------------------------------------------------------- */
// Constructor
var XfdVenue = function(type, settings) {
this.type = type;
for ( var key in settings ) {
this[key] = settings[key];
}
};
// ---------- XfdVenue prototype --------------------------------------------------------------- */
XfdVenue.prototype.hasNomTemplate = function(wikitext) {
var pattern = new RegExp(this.regex.nomTemplate);
return pattern.test(wikitext);
};
XfdVenue.prototype.removeNomTemplate = function(wikitext) {
var pattern = new RegExp(this.regex.nomTemplate);
var matches = wikitext.match(pattern);
if ( !matches ) {
return wikitext;
}
if ( matches.length > 1 ) {
throw new Error('Multiple nomination templates on page');
}
return wikitext.replace(pattern, '');
};
XfdVenue.prototype.updateNomTemplateAfterRelist = function(wikitext, today, sectionHeader) {
var matches = wikitext.match(this.regex.relistPattern);
if ( !matches ) {
return wikitext;
}
if ( matches.length > 1 ) {
throw new Error('Multiple nomination templates on page');
}
return wikitext.replace(
this.regex.relistPattern,
this.wikitext.relistReplace
.replace("__TODAY__", today)
.replace("__SECTION_HEADER__", sectionHeader)
);
};
// ---------- Venue-specific instances ----------------------------------------------------------- */
// MFD
XfdVenue.Mfd = function() {
var venue = new XfdVenue('mfd', {
path: 'Wikipedia:Miscellany for deletion',
subpagePath: 'Wikipedia:Miscellany for deletion/',
ns_number: null,
html: {
Line 197 ⟶ 241:
},
wikitext: {
closeTop: "{{subst:Mfd top
closeBottom: "{{subst:Mfd bottom}}",
oldXfd: "{{Old MfD |date=__DATE__ |result='''__RESULT__''' |page=__SUBPAGE__}}"+
Line 203 ⟶ 247:
mergeFrom: "{{mfd-mergefrom|__NOMINATED__|__DEBATE__|__DATE__}}\n",
mergeTo: "{{mfd-mergeto|__TARGET__|__DEBATE__|__DATE__|__TARGETTALK__}}\n",
alreadyClosed: "{{#ifeq:{{FULLPAGENAME}}|
"{{collapse bottom}}|}}"
},
regex: {
nomTemplate: /(?:<noinclude>\s*)?(?:{{mfd[^}}]*}}|<span id="mfd".*?<\/span> \[\[
}
});
return venue;
};
// CFD
XfdVenue.Cfd = function() {
var venue = new XfdVenue('cfd', {
path: 'Wikipedia:Categories for discussion/Log/',
ns_number: [14],
html: {
head: 'h4',
Line 231 ⟶ 273:
oldXfd: "{{Old CfD |__SECTION__ |date=__DATE__ |action=__ACTION__ "+
"|result=__RESULT__}}\n",
alreadyClosed: "<!-- Template:Cfd top -->"
relistReplace: " full|day=__DAY__|month=__MONTH__|year=__YEAR__",
},
nomTemplate: /<!--\s*BEGIN CFD TEMPLATE\s*-->(?:.|\n)+<!--\s*END CFD TEMPLATE\s*-->\n*/gi,
relistPattern: / full\|day\=\d\d?\|month=\w+\|year\=\d{4}/gi
}
});
// Override prototype
venue.updateNomTemplateAfterRelist = function(wikitext, today, _sectionHeader) {
var matches = wikitext.match(venue.regex.relistPattern);
if ( !matches ) {
return wikitext;
}
if ( matches.length > 1 ) {
throw new Error('Multiple nomination templates on page');
}
var todayParts = today.split(' ');
return wikitext.replace(
venue.regex.relistPattern,
venue.wikitext.relistReplace
.replace("__DAY__", todayParts[2])
.replace("__MONTH__", todayParts[1])
.replace("__YEAR__", todayParts[0])
)
.replace( // {{cfc}} is a bit different to the other CFD nomination template
/\'\'\'\[\[Wikipedia\:Categories for discussion\/Log\/\d{4} \w+ \d{1,2}\#/,
"'''[[Wikipedia:Categories for discussion/Log/" + today + "#"
);
};
return venue;
};
// FFD
XfdVenue.Ffd = function() {
var venue = new XfdVenue('ffd', {
ns_number: [6],
ns_unlink: ['0', '10', '100', '118'], // main, Template, Portal, Draft
html: {
Line 263 ⟶ 330:
nomTemplate: /{{ffd[^}}]*}}/gi,
relistPattern: /{{\s*ffd\s*\|\s*log\s*=\s*[^|}}]*/gi
}
});
return venue;
};
// TFD
XfdVenue.Tfd = function() {
var venue = new XfdVenue('tfd', {
path: 'Wikipedia:Templates for discussion/Log/',
subpagePath: 'Wikipedia:Templates for discussion/',
ns_number: [10, 828],
html: {
head: 'h4',
Line 287 ⟶ 352:
oldXfd: "{{oldtfdfull|date= __DATE__ |result=__RESULT__ |disc=__SECTION__}}\n",
pagelinks: "* {{tfd links|__PAGE__}}\n",
relistReplace: "
alreadyClosed: "<!-- Tfd top -->"
},
regex: {
nomTemplate: /(
relistPattern: /
},
holdingCellSectionNumber: {
Line 311 ⟶ 373:
"merge-meta": 10
}
});
// Override prototype
venue.removeNomTemplate = function(wikitext) {
var pattern = new RegExp(venue.regex.nomTemplate);
var matches = wikitext.match(pattern);
if ( !matches ) {
return wikitext;
}
if ( matches.length > 1 ) {
throw new Error('Multiple nomination templates on page');
}
var tags = pattern.exec(wikitext);
if ( !tags ) {
return wikitext;
}
var logical_xor = function(first, second) {
return (first ? true : false) !== (second ? true : false);
};
var unbalancedNoincludeTags = logical_xor(tags[1], tags[2]);
var replacement = ( unbalancedNoincludeTags ) ? "$1$2" : "";
return wikitext.replace(pattern, replacement);
};
venue.updateNomTemplateAfterRelist = function(wikitext, today, sectionHeader) {
var matches = wikitext.match(venue.regex.relistPattern);
if ( !matches ) {
return wikitext;
}
if ( matches.length > 1 ) {
throw new Error('Multiple nomination templates on page');
}
return wikitext.replace(
venue.regex.relistPattern,
venue.wikitext.relistReplace
.replace("__TODAY__", today)
.replace("__SECTION_HEADER__", sectionHeader)
);
};
return venue;
};
// RFD
XfdVenue.Rfd = function() {
var venue = new XfdVenue('rfd', {
type: 'rfd',
path: 'Wikipedia:Redirects for discussion/Log/',
ns_number: null,
html: {
Line 329 ⟶ 429:
oldXfd: "{{Old RfD |date={{subst:date|__FIRSTDATE__}} |result='''__RESULT__'''"+
" |page=__DATE__#__SECTION__}}\n",
alreadyClosed: "<!-- Template:Rfd top-->"
relistReplace: "#invoke:RfD||2=__SECTION_HEADER__|"
},
regex: {
nomTemplate: /(^\s*{{.*#invoke:RfD(?:.|\n)*?-->\|content=\n?|\n?<!-- Don't add anything after this line.*? -->\n}}|\[\[Category:Temporary maintenance holdings\]\]\n?)/g,
fullNomTemplate: /(^\s*{{.*#invoke:RfD(?:.|\n)*?<!-- Don't add anything after this line.*? -->\n}}|\[\[Category:Temporary maintenance holdings\]\]\n?)/g,
relistPattern: /#invoke:RfD\|\|\|/gi
},
});
// Override prototype
venue.removeNomTemplate = function(wikitext) {
var pattern = new RegExp(venue.regex.nomTemplate);
return wikitext.replace(pattern, '');
};
return venue;
};
// AFD
XfdVenue.Afd = function(transcludedOnly) {
var venue = new XfdVenue('afd', {
type: 'afd',
path: '
subpagePath: '
ns_number: [0], // main
ns_logpages: 4, // Wikipedia
ns_unlink: ['0', '10', '100', '118'], // main, Template, Portal, Draft
Line 368 ⟶ 472:
nomTemplate: /(?:{{[Aa]rticle for deletion\/dated|<!-- Please do not remove or change this AfD message)(?:.|\n)*?}}(?:(?:.|\n)+this\ point -->)?\s*/g
},
transcludedOnly: transcludedOnly
});
return venue;
XfdVenue.newFromPageName = function(pageName) {
// Create xfd venue object for this page
var isAfd = /(Articles_for_deletion|User:Cyberbot_I|Wikipedia:WikiProject_Deletion_sorting)/.test(pageName);
var afdTranscludedOnly = /(User:Cyberbot_I|Wikipedia:WikiProject_Deletion_sorting)/.test(pageName);
if ( pageName.includes('Wikipedia:Miscellany_for_deletion') ) {
return XfdVenue.Mfd();
} else if ( pageName.includes('Categories_for_discussion/') ) {
return XfdVenue.Cfd();
} else if ( pageName.includes('Files_for_discussion') ) {
return XfdVenue.Ffd();
} else if ( pageName.includes('Templates_for_discussion') ) {
return XfdVenue.Tfd();
} else if ( pageName.includes('Redirects_for_discussion') ) {
return XfdVenue.Rfd();
} else if ( isAfd ) {
return XfdVenue.Afd(afdTranscludedOnly);
} else {
throw new Error('"' + pageName + '" is not an XFD page');
}
};
config.xfd = XfdVenue.newFromPageName(config.mw.wgPageName);
if (window.XFDC_SANDBOX) config = window.XFDC_MAKE_SANDBOX_CONFIG(config); // Adjust some settings if running in sandbox mode
/* ========== API =============================================================================== */
var API = new mw.Api( {
'Api-User-Agent': 'XFDcloser/' + config.script.version +
' ( https://
}
}
} );
// Helper functions for processing results
var arrayFromResponsePages = function(response) {
return $.map(response.query.pages, function(page) { return page; });
};
return arrayFromResponsePages(response)[0];
};
API.editWithRetry = function(titles, getParams, transform, onEachSuccess, onEachFail) {
var processPage = function(page, id, starttime) {
var basetimestamp = page.revisions && page.revisions[0].timestamp;
var simplifiedPage = {
pageid: page.pageid,
missing: page.missing === "",
redirect: page.redirect === "",
categories: page.categories,
ns: page.ns,
title: page.title,
content: page.revisions && page.revisions[0].slots.main["*"]
};
return $.when( transform(simplifiedPage) )
.then(function(editParams) {
title: page.title,
// Protect against errors and conflicts
assert: 'user',
basetimestamp: basetimestamp,
starttimestamp: starttime
}, editParams );
var doEdit = function(isRetry) {
return API.postWithToken('csrf', query)
.then(
function(data) {
if (onEachSuccess) {
onEachSuccess(data);
}
return data.edit;
},
function(code, error) {
if (code === 'http' && !isRetry) {
return doEdit(true);
} else if ( code === 'editconflict' ) {
return doGetQuery(page.title);
}
if (onEachFail) {
onEachFail(code, error, page.title);
}
return $.Deferred().reject(code, error, page.title);
}
);
};
return doEdit();
}, function(code, error) {
if (onEachFail) {
onEachFail(code, error, page.title);
}
return $.Deferred().reject(code, error, page.title);
});
};
var doGetQuery = function(titles, isRetry) {
return API.get(
$.extend(
{
"action": "query",
"format": "json",
"curtimestamp": 1,
"titles": titles,
"prop": "revisions|info",
"rvprop": "content|timestamp",
"rvslots": "main"
},
getParams
)
).then(function(response) {
var starttime = response.curtimestamp;
// Get an array of promises of edits (which may either be resolved or rejected)
var pages = $.map(response.query.pages, function(page, id) {
return processPage(page, id, starttime);
});
// If only for one title, return that promise (which may either be resolved or rejected)
if (!$.isArray(titles)) {
return pages[0];
}
// Otherwise, convert the array of promises into a single promise, resolved if all were
// resolved, or rejected with an array of errors of all that failed.
return $.when.apply(
null,
// Force promises to resolve as an object with a `success` key, so that
// $.when will wait for all of them to be resolved or rejected
pages.map(function(page) {
return page.then(
function(success) {
return {success: true};
},
function(code, error, title) {
return {
success: false,
code: code,
error: error,
title: title
};
}
); // end page.then
}) // end page.map
) // end $.when
.then(function() {
var args = Array.prototype.slice.call(arguments);
var errors = args.filter(function(arg) {
return !arg.success;
});
if (errors.length > 0) {
return $.Deferred().reject("write", errors);
}
return true;
});
}, function(code, error) {
if (!isRetry) {
return doGetQuery(titles, true);
}
return $.Deferred().reject("read", code, error);
});
};
return doGetQuery(titles);
};
var dmyDateString = function(date) {
return date.getUTCDate().toString() + ' ' +
config.monthNames[date.getUTCMonth()] + ' ' +
date.getUTCFullYear().toString();
};
/**
* Generates a JS Date object from the text of a timestamp
* @param {String} sigTimestamp in format "`hh`:`mm`, `d` `Month` `yyyy` (UTC)", e.g. "09:42, 11 January 2019 (UTC)"
* @returns {Date|NaN} Date object, or NaN if sigTimestamp could not be parsed
*/
var dateFromSigTimestamp = function(sigTimestamp) {
var pattern = /(\d\d\:\d\d), (\d{1,2}) (\w+) (\d\d\d\d) \(UTC\)/;
var parts = pattern.exec(sigTimestamp);
if ( parts === null ) {
return NaN;
}
var year = parts[4];
var monthIndex = config.wgMonthNames.indexOf(parts[3]);
if ( monthIndex === -1 ) {
return NaN;
}
var month = ( monthIndex < 10 )
? '0' + monthIndex
: monthIndex;
var day = ( parts[2].length === 1 )
? '0' + parts[2]
: parts[2];
var time = 'T' + parts[1] + 'Z';
var iso8601DateString = year + '-' + month + '-' + day + time;
return Date.parse(iso8601DateString) && new Date(iso8601DateString);
};
/* ========== Additional functions for working with mw.Title objects ============================ */
var hasCorrectNamespace = function(mwTitleObject) {
return (
config.xfd.ns_number === null ||
config.xfd.ns_number.includes(mwTitleObject.getNamespaceId())
);
};
var setExistence = function(mwTitleObject, exists) {
mw.Title.exist.set(mwTitleObject.toString(), exists);
};
/* ========== Multibutton confirm dialog ======================================================== */
/** multiButtonConfirm
* @param {Object} config
* @config {String} title Title for the dialogue
* @config {String} message Message for the dialogue. HTML tags (except for <br>, <p>, <ul>,
* <li>, <hr>, and <pre> tags) are escaped; wikilinks are turned into real links.
* @config {Array} actions Optional. Array of configuration objects for OO.ui.ActionWidget
* <https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.ActionWidget>.
* If not specified, the default actions are 'accept' (with label 'OK') and 'reject' (with
* label 'Cancel').
* @config {String} size Symbolic name of the dialog size: small, medium, large, larger or full.
* @return {Promise<String>} action taken by user
*/
var multiButtonConfirm = function(config) {
var dialogClosed = $.Deferred();
// Wrap message in a HtmlSnippet to prevent escaping
var htmlSnippetMessage = new OO.ui.HtmlSnippet(
safeUnescape(config.message)
);
var windowManager = new OO.ui.WindowManager();
var messageDialog = new OO.ui.MessageDialog();
$('body').append( windowManager.$element );
windowManager.addWindows( [ messageDialog ] );
windowManager.openWindow( messageDialog, {
'title': config.title,
'message': htmlSnippetMessage,
'actions': config.actions,
'size': config.size
} );
windowManager.on('closing', function(_win, promise) {
promise.then(function(data) {
dialogClosed.resolve(data && data.action);
windowManager.destroy();
});
});
return dialogClosed.promise();
};
/* ========== Discusssion class =================================================================
Each instance represents an XfD discussion.
---------------------------------------------------------------------------------------------- */
/
* @param {Object} discussionConfig configuration object:
* @config {String} id - A unique ID for this discussion (used in various elements' IDs)
* @config {String} nomPage - Page where the XFD discussion is taking place; either a dated or
* name-based subpage
* @config {String} sectionHeader - text of the section heading for the XFD discussion
* @config {String} sectionNumber - edit section number for the XFD discussion
* @config {mw.Title[]} pages - pages nominated in the XFD discussion; omit if in basic mode
* @config {Boolean} isOld - `true` if discussion has been listed (or relisted) for more than 7 days
* @config {Boolean} isRelisted - `true` if discussion has been relisted
*
*/
var Discussion = function(discussionConfig) {
var defaultConfig = {
pages: [],
deferred: {} // For later tracking of jQuery Deferred objects
};
$.extend(this, defaultConfig, discussionConfig);
};
// Construct from headline span element
Line 466 ⟶ 757:
var $headlineSpan = $(context);
var $heading = $headlineSpan.parent();
var $statusContainer = config.isMobileSite ? $heading.next() : $heading;
// Fix for "Auto-number headings" preference
Line 471 ⟶ 763:
// Get section header
var sectionHeader = $headlineSpan.text().trim();
// Check if already closed. Closed AfDs and MfDs have the box above the heading.
if ( /(afd|mfd)/.test(config.xfd.type) && $heading.parent().attr('class') && $heading.parent().attr('class').includes('xfd-closed') ) {
// Skip
return;
Line 486 ⟶ 778:
var sectionlink = $heading.find('.mw-editsection a')
.not('.mw-editsection-visualeditor, .autoCloserButton').attr('href');
if (!sectionlink) {
// Try to find a section link generated by Module:XfD_old.
sectionlink = $heading.next().find(".xfdOldSectionEdit > a").attr("href");
if (!sectionlink) {
// XFDcloser can't work without knowing the nompage and section number, so skip this section.
return;
}
// Add a "T-" so the next check will see this as a transcluded section
sectionlink = sectionlink.replace("section=", "section=T-");
}
var editsection = sectionlink.split('section=')[1].split('&')[0];
var nompage = '';
if ( /T/.test(editsection) ) {
// Section is transcluded from another page
nompage =
decodeURIComponent(sectionlink.split("title=")[1].split("&")[0])
).
if ( -1 !== $.inArray(nompage, [
'Wikipedia:Redirects for discussion/Header',
'Wikipedia:Redirect/Deletion reasons',
'Wikipedia:Templates for discussion/Holding cell',
'Wikipedia:Categories for discussion/Speedy'
])
) {
Line 509 ⟶ 812:
return;
}
nompage =
}
var pages=[];
var
if ( config.xfd.type === 'cfd' ) {
//CFDs: Nominates pages are the first link of an <li> item in a <ul> list, within a <dl> list
pages =
.nextUntil(config.xfd.html.head + ', div.xfd-closed')
.find('dd > ul > li')
.has('b:first-child:contains("Propose ")')
.find('a:first-of-type')
.not('.external')
.map(function() { return mw.Title.newFromText($(this).text()); })
.get();
if ( pages.length === 0 ) {
// Sometimes nominated pages are instead just in a <ul> list, e.g.
// Wikipedia:Categories_for_discussion/Log/2019_February_5#Foo_in_fiction
pages = $heading
.next('ul')
.find('li')
.find('a:first-of-type')
.not('.external')
.map(function() { return mw.Title.newFromText($(this).text()); })
.get();
}
} else if ( config.xfd.type === 'rfd' || config.xfd.type === 'mfd' ) {
// For MFD, closed discussion are within a collapsed table
Line 530 ⟶ 851:
.children('a, span.plainlinks:not(.lx)')
.filter(':first-child')
.map(function() { return
.get();
if ( config.xfd.type === 'rfd' ) {
var
.nextUntil(config.xfd.html.head + ', div.xfd-closed, table.xfd-closed')
.
// Fix for "Comments in Local Time" gadget
discussionNodes.find('span.localcomments').each(function(){
var utcTime = $(this).attr('title');
$(this).text(utcTime);
});
var discussionText = discussionNodes.text();
// Ignore relisted discussions, and non-boxed closed discussions
if (
discussionText.includes('
discussionText.includes('
) {
return;
}
// Find first timestamp date
var
var
}
} else {
Line 556 ⟶ 885:
.children('a')
.filter(':first-child')
.map(function() { return
.get();
}
Line 565 ⟶ 894:
pages = false;
}
// Check discussion age (since last relist, if applicable)
// TODO: reduce redundancy with finding RfDs' first date
var isOld;
var $discussionNodes = $heading
.nextUntil(config.xfd.html.head + ', div.xfd-closed, table.xfd-closed')
.clone()
.find('span.localcomments')
.each(function(){
var utcTime = $(this).attr('title');
$(this).text(utcTime);
})
.end();
var lastRelist = $('<div>').append($discussionNodes).find('.xfd_relist').last().text();
if ( lastRelist ) {
$statusContainer.addClass('xfdc-relisted');
}
var notTranscludedCorrectlyPatt = /(?:Automated|Procedural) (?:comment|Note).*transcluded.*/i;
var notTranscludedCorrectlyMatch = $discussionNodes.text().match(notTranscludedCorrectlyPatt);
var notTranscludedCorrectlyComment = notTranscludedCorrectlyMatch && notTranscludedCorrectlyMatch[0];
var timestampPatt = /\d\d:\d\d, \d{1,2} \w+ \d{4} \(UTC\)/;
var listingTimestampMatch = lastRelist.match(timestampPatt) ||
notTranscludedCorrectlyComment && notTranscludedCorrectlyComment.match(timestampPatt) ||
$discussionNodes.text().match(timestampPatt);
var listingTimestampDate = listingTimestampMatch && dateFromSigTimestamp(listingTimestampMatch[0]);
if ( !listingTimestampDate ) {
$statusContainer.addClass('xfdc-unknownAge');
} else {
var millisecondsSinceListing = new Date() - listingTimestampDate;
var discussionRuntimeDays = 7;
var discussionRuntimeMilliseconds = discussionRuntimeDays * 24 * 60 * 60 * 1000;
isOld = millisecondsSinceListing > discussionRuntimeMilliseconds;
$statusContainer.addClass((isOld ? 'xfdc-old' : 'xfdc-notOld'));
}
// Create status span and notices div with unique id based on headingIndex
var uniqueID = 'XFDC' + headingIndex;
var $statusSpan = $('<span>')
.attr({'id':uniqueID, 'class':'xfdc-status'})
.text('[XFDcloser
var $noticesDiv = $('<div>').attr({'id':uniqueID+'-notices', 'class':'xfdc-notices'});
if (config.isMobileSite) {
$heading.next().prepend( $statusSpan, $noticesDiv );
} else {
$headlineSpan.after( $statusSpan );
$heading.after($noticesDiv);
}
// Create discussion object
return new Discussion(
'id': uniqueID,
'nomPage': nompage,
'sectionHeader': sectionHeader,
'sectionNumber': editsection,
'pages': pages || [],
'firstDate': firstDate || null,
'isOld': !!isOld,
'isRelisted': !!lastRelist
});
};
// ---------- Discusssion prototype ------------------------------------------------------------- */
Line 590 ⟶ 964:
this.get$status().empty().append($status);
};
/**
* Open dialog
*
* @param {Boolean} relisting open in relisting mode
* @returns {Boolean} Dialog was opened
*/
Discussion.prototype.openDialog = function(relisting) {
if (document.getElementById('xfdc-dialog')) {
// Another dialog is already open
return false;
}
this.dialog = new Dialog(this, !!relisting);
this.dialog.setup();
return true;
};
// Mark as finished
Line 602 ⟶ 986:
if ( aborted != null ) {
msg = [
$('<strong>').text( ( self.dialog && self.dialog.relisting ) ? '
( aborted === '' ) ? '' : ': ' + aborted
];
} else if ( self.dialog && self.dialog.relisting ) {
msg = [
'
$('<strong>').text('
' (reload page to see the actual relist)'
];
} else {
msg = [
'
$('<strong>').text(self.taskManager.inputData.getResult()),
' (reload page to see the actual close)'
];
}
Line 630 ⟶ 1,014:
};
// Get an array of page titles
Discussion.prototype.getPageTitles = function(pagearray, options) {
return p.
});
if ( options && options.moduledocs ) {
return titles.map(function(t) {
var isModule = ( t.indexOf('Module:') === 0 );
return ( isModule ) ? t + '/doc' : t;
});
}
return titles;
};
// Get an array of page' talkpage titles (excluding pages which are themselves talkpages)
Discussion.prototype.getTalkTitles = function(pagearray) {
return (pagearray || this.pages).map(function(p) {
return p.
}).filter(function(t) { return t !== ''; });
};
// Get link text for a wikiink to the discussion - including anchor, except for AfDs/MfDs
Discussion.prototype.getNomPageLink = function() {
if (config.xfd.type === 'afd' || config.xfd.type === 'mfd') {
return this.nomPage;
} else {
return this.nomPage + '#' + mw.util.wikiUrlencode(this.sectionHeader).replace(/_/g, ' ');
}
};
Line 654 ⟶ 1,045:
};
// Get page object by matching the title
Discussion.prototype.getPageByTitle = function(
var convertModuleDoc = ( options && options.moduledocs && title.indexOf('Module:') === 0 );
var titleToCheck = ( convertModuleDoc ) ? title.replace(/\/doc$/,'') : title;
var search = mw.Title.newFromText(titleToCheck).getPrefixedText();
for ( var i=0; i<this.pages.length; i++ ) {
if ( search === this.pages[i].
return this.pages[i];
}
Line 665 ⟶ 1,059:
// Get page object by matching the talkpage's title
Discussion.prototype.getPageByTalkTitle = function(t) {
var search = mw.Title.newFromText
for ( var i=0; i<this.pages.length; i++ ) {
if ( search === this.pages[i].
return this.pages[i];
}
Line 685 ⟶ 1,079:
'[',
$('<a>')
.attr('title', '
.text('
']'
)
.click(function() {
return self.
});
Line 700 ⟶ 1,093:
'[',
$('<a>')
.attr({title:'
.text('
']'
)
.click(function() {
return self.openDialog(true) && self.setStatus('Relisting...');
});
// quickKeep
var $qk = $('<a>')
.attr('title', 'quickKeep: close as "keep", remove nomination templates, '+
'add old xfd templates to talk pages')
.text('
.click(function(){
var inputData = new InputData(self);
inputData.result = '
inputData.after = 'doKeepActions';
self.setStatus('
self.taskManager = new TaskManager(self, inputData);
self.taskManager.start();
Line 727 ⟶ 1,119:
var $qd = ( !config.user.isSysop && config.xfd.type !== 'tfd' ) ? '' : $('<a>')
.attr({
'title': 'quickDelete: close as "delete", delete nominated pages & their talk pages'+
(( config.xfd.type === 'rfd' ) ? '' :' &
(( config.xfd.type === 'afd' || config.xfd.type === 'ffd' ) ? ',
'unlink backlinks' : ''),
'class': 'xfdc-qd'
})
.text('
if ( !config.user.isSysop && config.xfd.type == 'tfd' ) {
$qd.attr('title', 'quickDelete: close as "delete", tag nominated templates with '+
'{{being deleted}}, add nominated templates to the holding cell as "orphan"')
.click(function(){
var inputData = new InputData(self);
inputData.result = '
inputData.after = 'holdingCell';
inputData.holdcell = 'orphan';
inputData.dontdeletetalk = true;
self.setStatus('
self.taskManager = new TaskManager(self, inputData);
self.taskManager.start();
Line 749 ⟶ 1,142:
} else if ( config.user.isSysop ) {
$qd.click(function(){
$.when(config.xfd.type === 'tfd' ?
multiButtonConfirm({
title: 'Really delete?',
message: 'Deletion will not remove transclusions from articles. Do you want to use the holding cell instead?',
actions: [
{ label: 'Cancel', flags: 'safe' },
{ label: 'Delete', flags: 'destructive', action: 'delete' },
{ label: 'Holding cell', flags: 'progressive', action: 'holdcell' }
],
size: 'medium'
}) :
'delete'
)
.then(function(action) {
var inputData = new InputData(self);
inputData.result = 'delete';
if ( action === 'delete' ) {
inputData.after = 'doDeleteActions';
inputData.deleteredir = ( config.xfd.type === 'rfd' ) ? null : true;
inputData.unlinkbackl = ( config.xfd.type === 'afd' || config.xfd.type === 'ffd' ) ? true : null;
} else if ( action === 'holdcell' ) {
inputData.after = 'holdingCell';
inputData.holdcell = 'orphan';
inputData.dontdeletetalk = true;
} else {
// User selected Cancel
return;
}
self.setStatus('Closing...');
self.taskManager = new TaskManager(self, inputData);
self.taskManager.start();
});
});
}
Line 770 ⟶ 1,185:
$('<a>')
.attr('title', 'quickClose discussion...')
.text('
.click(function(){
$(this).hide().next().show();
Line 785 ⟶ 1,200:
' ',
$('<span>')
.attr({title: '
.html(" x ")
.click(function(){
Line 798 ⟶ 1,213:
self.setStatus([
$close,
( self.
additonal || ''
]);
Line 808 ⟶ 1,223:
// Preserve reference to discussion object
var self = this;
var pagesExistencesPromise = ( self.getPageTitles().length > 50 )
? $.Deferred().reject('Too many pages')
: API.get( {
action: 'query',
titles: self.getPageTitles().join('|'),
prop: 'info',
inprop: 'talkid'
.then(arrayFromResponsePages)
.then(function(pages) {
pages.forEach(function(page) {
var pageObject = self.getPageByTitle(page.title);
if ( !pageObject ) {
return $.Deferred().reject('Unexpacted title `'+page.title+'`');
}
var pageExists = page.pageid > 0;
var talkExists = page.talkid > 0;
setExistence(pageObject, pageExists);
setExistence(pageObject.getTalkPage(), talkExists);
return true;
});
var nominationDatePromise = ( config.xfd.type !== "afd" && config.xfd.type !== "mfd" )
? $.Deferred().resolve(self.nomPage.split(config.xfd.path)[1])
: API.get({
action: 'query',
titles: self.nomPage,
Line 894 ⟶ 1,255:
rvprop: 'timestamp',
rvdir: 'newer',
rvlimit: '1'
})
.then(pageFromResponse)
.then(function(page) {
var revisionDate = new Date(page.revisions[0].timestamp);
return dmyDateString(revisionDate);
});
nominationDatePromise.then(function(nomDate) {
self.nomDate = nomDate;
// For an RfD with no first comment date detected, use the nom page date in dmy format
if ( config.xfd.type === "rfd" && !self.firstDate ) {
self.firstDate = nomDate.replace(/(\d+) (\w*) (\d+)/g, "$3 $2 $1");
}
});
return $.when(pagesExistencesPromise, nominationDatePromise).then(
function(){ return ''; },
function(failMessage, jqxhr) {
return $('<span>').addClass('xfdc-notice-error').append(
'Error retrieving page information (reload the page to try again) ',
$('<span>').addClass('xfdc-notice-error').append(
extraJs.makeErrorMsg(failMessage, jqxhr)
)
);
}
);
};
// Check if discussion is in 'basic' mode - i.e. no pages
Discussion.prototype.isBasicMode = function() {
return !this.pages || this.pages.length === 0;
};
Line 926 ⟶ 1,305:
this.interfaceWindow.setTitle( 'XFDcloser' );
this.interfaceWindow.setScriptName('(v' + config.script.version + ')');
this.interfaceWindow.addFooterLink('
this.interfaceWindow.addFooterLink('
this.interfaceWindow.setContent(
$('<div>')
Line 938 ⟶ 1,317:
.get(0)
);
$(this.interfaceWindow.content).parent().parent().find('a.ui-dialog-titlebar-close.ui-corner-all').remove();
$('#xfdc-dialog').parent().css('background-color', '#f0f0f0');
};
// ---------- Rcat multiselect widget ----------------------------------------------------------- */
/**
* Dialog.RcatMultiselect
*
* Creates a modified OOUI MenuTagMultiselectWidget with Rcat templates as options,
* grouped as per https://en.wikipedia.org/wiki/Template:R_template_index
*
* @requires {Modules} oojs-ui-core, oojs-ui-widgets
* @param {function} onChangeCallback({String[]} datas)
* Callback function for when the selected rcats change, is passed an array of Strings
* each of which is a selected rcat template (including braces `{{` and `}}`). When all
* prior selections are removed, an empty array is passed.
* @param {object} context
* The context to be used as the `this` value when executing the callback function
* @param {jQuery} overlay
* Overlay element. If ommited, element with id `ejs-rcats-overlay` will be emptied and used
* if it exists, or else a div with that id will be created and used.
* @return {LookupMenuTagMultiselectWidget}
*/
Dialog.RcatMultiselect = function(onChangeCallback, context, overlay) {
// Extend OO.ui.MenuSelectWidget to use a more intuitive lookup system, such that
// e.g. typing "section" brings up `{{R to section}}` as a match
var LookupMenuSelectWidget = function(config) {
OO.ui.MenuSelectWidget.call(this, config);
};
LookupMenuSelectWidget.prototype = Object.create(OO.ui.MenuSelectWidget.prototype);
LookupMenuSelectWidget.prototype.getItemMatcher = function ( s, exact ) {
var re;
if ( s.normalize ) {
s = s.normalize();
}
s = exact ? s.trim() : s.replace( /^\s+/, '' );
re = s.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
if ( exact ) {
re = '^\\s*' + re + '\\s*$';
}
re = new RegExp( re, 'i' );
return function ( item ) {
var matchText = item.getMatchText();
if ( matchText.normalize ) {
matchText = matchText.normalize();
}
return re.test( matchText );
};
};
// Extend OO.ui.MenuTagMultiselectWidget to use a LookupMenuSelectWidget instead of
// the standard MenuSelectWidget
var LookupMenuTagMultiselectWidget = function ( config ) {
OO.ui.MenuTagMultiselectWidget.call(this, config);
// Override menu widget
this.menu = new LookupMenuSelectWidget( $.extend(
{
widget: this,
input: this.hasInput ? this.input : null,
$input: this.hasInput ? this.input.$input : null,
filterFromInput: !!this.hasInput,
$autoCloseIgnore: this.hasInput ?
this.input.$element : $( [] ),
$floatableContainer: this.hasInput && this.inputPosition === 'outline' ?
this.input.$element : this.$element,
$overlay: this.$overlay,
disabled: this.isDisabled()
},
config.menu
) );
this.menu.connect( this, {
choose: 'onMenuChoose',
toggle: 'onMenuToggle'
} );
if ( this.hasInput ) {
this.input.connect( this, { change: 'onInputChange' } );
}
if ( this.$input ) {
this.$input.prop( 'disabled', this.isDisabled() );
this.$input.attr( {
role: 'combobox',
'aria-owns': this.menu.getElementId(),
'aria-autocomplete': 'list'
} );
}
if ( !this.popup ) {
this.$content.append( this.$input );
this.$overlay.append( this.menu.$element );
}
//this.onMenuItemsChange();
};
LookupMenuTagMultiselectWidget.prototype = Object.create(OO.ui.MenuTagMultiselectWidget.prototype);
// Define new overlay, or use/reuse already defined element
if ( overlay == null ) {
if ( $('#ejs-rcats-overlay').length ) {
overlay = $('#ejs-rcats-overlay').first().empty();
} else {
overlay = $('<div>')
.attr('id', 'ejs-rcats-overlay')
.css({'z-index':'2000', 'position':'fixed', 'top':'0px', 'font-size':'85%'})
.prependTo('body');
}
}
// Make menu option widgets based on template name
var newRcatOption = function(val) {
return new OO.ui.MenuOptionWidget( {data: '{{'+val+'}}', label: '{{'+val+'}}'} );
};
// Make the widget
var rcatMultiselectWidget = new LookupMenuTagMultiselectWidget( {
allowArbitrary: true,
$overlay: overlay,
popup: false,
menu: {
items: [
// Common Rcats
new OO.ui.MenuSectionOptionWidget({label:'Common'}),
newRcatOption('R to related topic'),
newRcatOption('R from subtopic'),
newRcatOption('R to list entry'),
newRcatOption('R to section'),
new OO.ui.MenuSectionOptionWidget({label:'Related information'}),
newRcatOption('R from album'),
newRcatOption('R to article without mention'),
newRcatOption('R from book'),
newRcatOption('R to decade'),
newRcatOption('R from domain name'),
newRcatOption('R from top-level domain'),
newRcatOption('R from film'),
newRcatOption('R from gender'),
newRcatOption('R from list topic'),
newRcatOption('R from member'),
newRcatOption('R to related topic'),
newRcatOption('R from related word'),
newRcatOption('R from phrase'),
newRcatOption('R from school'),
newRcatOption('R from song'),
newRcatOption('R from subtopic'),
newRcatOption('R to subtopic'),
newRcatOption('R from Unicode'),
new OO.ui.MenuSectionOptionWidget({label:'Fiction'}),
newRcatOption('R from fictional character'),
newRcatOption('R from fictional element'),
newRcatOption('R from fictional location'),
newRcatOption('R to TV episode list entry'),
// Grammar, punctuation, and spelling
new OO.ui.MenuSectionOptionWidget({label:'Abbreviation'}),
newRcatOption('R to acronym'),
newRcatOption('R from acronym'),
newRcatOption('R to initialism'),
newRcatOption('R from initialism'),
new OO.ui.MenuSectionOptionWidget({label:'Capitalisation'}),
newRcatOption('R from CamelCase'),
newRcatOption('R from other capitalisation'),
newRcatOption('R from miscapitalisation'),
new OO.ui.MenuSectionOptionWidget({label:'Grammar & punctuation'}),
newRcatOption('R from modification'),
newRcatOption('R from plural'),
newRcatOption('R to plural'),
new OO.ui.MenuSectionOptionWidget({label:'Parts of speech'}),
newRcatOption('R from adjective'),
newRcatOption('R from adverb'),
newRcatOption('R from common noun'),
newRcatOption('R from gerund'),
newRcatOption('R from proper noun'),
newRcatOption('R from verb'),
new OO.ui.MenuSectionOptionWidget({label:'Spelling'}),
newRcatOption('R from alternative spelling'),
newRcatOption('R from ASCII-only'),
newRcatOption('R to ASCII-only'),
newRcatOption('R from diacritic'),
newRcatOption('R to diacritic'),
newRcatOption('R from misspelling'),
newRcatOption('R from stylization'),
// Alternative names
new OO.ui.MenuSectionOptionWidget({label:'Alternative names (general)'}),
newRcatOption('R from alternative language'),
newRcatOption('R from alternative name'),
newRcatOption('R from former name'),
newRcatOption('R from historic name'),
newRcatOption('R from incorrect name'),
newRcatOption('R from long name'),
newRcatOption('R from portmanteau'),
newRcatOption('R from short name'),
newRcatOption('R from sort name'),
newRcatOption('R from less specific name}'),
newRcatOption('R from more specific name'),
newRcatOption('R from synonym'),
newRcatOption('R from antonym'),
new OO.ui.MenuSectionOptionWidget({label:'Alternative names (people)'}),
newRcatOption('R from birth name'),
newRcatOption('R from given name'),
newRcatOption('R to joint biography'),
newRcatOption('R from married name'),
newRcatOption('R from name with title'),
newRcatOption('R from personal name'),
newRcatOption('R from pseudonym'),
newRcatOption('R from surname'),
new OO.ui.MenuSectionOptionWidget({label:'Alternative names (technical)'}),
newRcatOption('R from Java package name'),
newRcatOption('R from molecular formula'),
newRcatOption('R from technical name'),
newRcatOption('R to technical name'),
newRcatOption('R from trade name'),
new OO.ui.MenuSectionOptionWidget({label:'Alternative names (organisms)'}),
newRcatOption('R from scientific name'),
newRcatOption('R from alternative scientific name'),
newRcatOption('R to scientific name'),
new OO.ui.MenuSectionOptionWidget({label:'Alternative names (geography)'}),
newRcatOption('R from name and country'),
newRcatOption('R from more specific geographic name'),
newRcatOption('R from postal code'),
// Navigation aids
new OO.ui.MenuSectionOptionWidget({label:'Navigation'}),
newRcatOption('R to anchor'),
newRcatOption('R avoided double redirect'),
newRcatOption('R from file metadata link'),
newRcatOption('R to list entry'),
newRcatOption('R mentioned in hatnote'),
newRcatOption('R to section'),
newRcatOption('R from shortcut'),
newRcatOption('R from template shortcut'),
new OO.ui.MenuSectionOptionWidget({label:'Disambiguation'}),
newRcatOption('R from ambiguous term'),
newRcatOption('R to anthroponymy page'),
newRcatOption('R to disambiguation page'),
newRcatOption('R from incomplete disambiguation'),
newRcatOption('R from incorrect disambiguation'),
newRcatOption('R from other disambiguation'),
newRcatOption('R from unnecessary disambiguation'),
new OO.ui.MenuSectionOptionWidget({label:'Merge, duplicate & move'}),
newRcatOption('R from duplicated article'),
newRcatOption('R with history'),
newRcatOption('R from merge'),
newRcatOption('R from move'),
newRcatOption('R with old history'),
new OO.ui.MenuSectionOptionWidget({label:'To namespaces'}),
newRcatOption('R to category namespace'),
newRcatOption('R to draft namespace'),
newRcatOption('R to help namespace'),
newRcatOption('R to main namespace'),
newRcatOption('R to portal namespace'),
newRcatOption('R to project namespace'),
newRcatOption('R to talk page'),
newRcatOption('R to template namespace'),
newRcatOption('R to user namespace'),
// Miscellaneous
new OO.ui.MenuSectionOptionWidget({label:'ISO codes'}),
newRcatOption('R from ISO 4'),
newRcatOption('R from ISO 639 code'),
newRcatOption('R from ISO 3166 code'),
newRcatOption('R from ISO 4217 code'),
newRcatOption('R from ISO 15924 code'),
new OO.ui.MenuSectionOptionWidget({label:'Miscellaneous'}),
newRcatOption('R printworthy'),
newRcatOption('R unprintworthy'),
newRcatOption('Wikidata redirect')
]
}
} );
// Execute callback when selector data changes
rcatMultiselectWidget.on('change', function(selectedOptions) {
onChangeCallback.call(
context,
selectedOptions.map(function(optionWidget) {
return optionWidget.data;
})
);
});
return rcatMultiselectWidget;
};
Line 976 ⟶ 1,630:
// --- Make interface elements: ---
Dialog.prototype.makeHeader = function(multimode, isRelisting) {
var self = this;
Line 986 ⟶ 1,640:
.attr('id', 'closeXFD-interface-header-action')
.append(
( self.relisting ) ? '
$('<em>').text(self.discussion.sectionHeader),
' ',
Line 997 ⟶ 1,651:
.css({'background':'#ff8', 'padding':'1px'})
.text(
( self.discussion.isBasicMode()
: self.discussion.pages.length + ( ( self.discussion.pages.length === 1 ) ? '
),
( multimode ) ? '' : ' ',
( multimode ) ? '' : $('<a>').attr('id', 'closeXFD-pagecount-show').text('[
( multimode ) ? '' : $('<a>').attr('id', 'closeXFD-pagecount-hide').text('[
)
)
Line 1,012 ⟶ 1,667:
// Multi/single-mode button
if ( !self.relisting && !self.discussion.
$header.prepend(
$('<button>')
.css('float', 'right')
.text(( multimode ) ? '
.click(function() {
if ( multimode ) {
Line 1,039 ⟶ 1,694:
.append(
$('<option>').attr('value', 'default').text(''),
$('<option>').attr('value', 'keep').text('
( config.xfd.type === 'ffd' ) ? '' : $('<option>')
.attr('value', 'redirect').text('
( config.xfd.type === 'ffd' || config.xfd.type === 'rfd' ) ? '' : $('<option>')
.attr('value', '
( config.xfd.type !== 'rfd' ) ? '' : $('<option>')
.attr('value', 'disambig').text('
$('<option>').attr('value', 'del').text('
$('<option>').attr('value', 'na').text('(
);
var $target = $('<span>')
Line 1,059 ⟶ 1,714:
// List of pages (or info on basic mode)
var isCfdClose = config.xfd.type === 'cfd' && !self.relisting;
var $pagesList = $('<ul>')
.attr('id', 'closeXFD-nominatedpages')
.css('font-size', '95%');
if ( !self.discussion.
for ( var i=0; i<self.discussion.pages.length; i++ ) {
var pageTitle = self.discussion.pages[i].
$('<li>')
.append(
Line 1,086 ⟶ 1,742:
$('<li>')
.append(
( !self.discussion.isBasicMode() && isCfdClose )
? 'Automated post-close actions are not available for CFDs.'
: 'Nominated pages were not detected.',
'You can still ',
( self.relisting ) ? 'relist' : 'close',
' this ',
config.xfd.type.toUpperCase(),
' discussion, but updating the nomination templates will need to be done manually '+
'–
extraJs.makeLink('WP:'+config.xfd.type.toUpperCase()+'AI'),
' for instructions.'
)
.appendTo($pagesList);
Line 1,107 ⟶ 1,766:
var t = $li.find('.closeXFD-pagelist-title').text();
self.inputData.pageActions[t].action = v;
if ( v === 'redirect' || v === '
$li.find('.closeXFD-pagelist-target').show();
} else {
Line 1,116 ⟶ 1,775:
$pagesList.find('input').change(function(){
var t = $(this).parent().parent().find('.closeXFD-pagelist-title').text();
self.inputData.pageActions[t].target =
$(this).val().trim()
);
Line 1,132 ⟶ 1,791:
$('<em>')
.append(
'See the ',
extraJs.makeLink('WP:NACD'),
' guideline for advice on appropriate and inappropriate closures'
),
$('<hr>')
Line 1,144 ⟶ 1,803:
var self = this;
var $resultContainer = $('<div>')
.attr('id', 'closeXFD-resultContainer')
.css({'float':'left', 'width':'99%', 'padding-bottom':'1%'})
.append(
$('<div>').append(
$('<strong>').text('
// Speedy
$('<span>').addClass('xfdc-dialog-bracketedOption').append(
Line 1,157 ⟶ 1,816:
self.inputData.speedy = ( $(this).prop('checked') );
}),
$('<label>').attr('for', 'closeXFD-speedy').text('
)
),
Line 1,164 ⟶ 1,823:
$('<span>').attr('class', 'xfdc-dialog-option').append(
$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
'class':'closeXFD-keepResult', 'id':'closeXFD-result-keep', 'value':'
$('<label>').attr('for', 'closeXFD-result-keep').text('
),
// Redirect
Line 1,175 ⟶ 1,834:
$('<label>')
.attr('for', 'closeXFD-result-redir')
.text(( config.xfd.type === 'rfd' ) ? '
( !config.user.isSysop ) ? '' : $('<label>')
.attr('for', 'closeXFD-result-redir')
.text(( config.xfd.type === 'rfd' ) ? '
.hide()
),
Line 1,187 ⟶ 1,846:
'class':'closeXFD-keepResult', 'id':'closeXFD-result-softredir',
'value':'soft redirect'}),
$('<label>').attr('for', 'closeXFD-result-softredir').text('
( !config.user.isSysop ) ? '' : $('<label>')
.attr('for', 'closeXFD-result-softredir').text('
),
// No consensus
Line 1,195 ⟶ 1,854:
$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
'class':'closeXFD-keepResult', 'id':'closeXFD-result-nocon',
'value':'
$('<label>').attr('for', 'closeXFD-result-nocon').text('
),
// Merge
Line 1,202 ⟶ 1,861:
.attr('class', 'xfdc-dialog-option').append(
$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
'id':'closeXFD-result-merge', 'value':'
$('<label>').attr('for', 'closeXFD-result-merge').text('
),
// Disambiguate
Line 1,210 ⟶ 1,869:
$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
'id':'closeXFD-result-disambig', 'value':'disambiguate'}),
$('<label>').attr('for', 'closeXFD-result-disambig').text('
),
// Delete
Line 1,216 ⟶ 1,875:
.attr('class', 'xfdc-dialog-option').append(
$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
'class':'closeXFD-deleteResult', 'id':'closeXFD-result-delete', 'value':'
$('<label>').attr('for', 'closeXFD-result-delete').text('
),
// Delete (disabled)
Line 1,225 ⟶ 1,884:
'class':'closeXFD-deleteResult', 'id':'closeXFD-result-delete-disabled',
'disabled':'disabled'}),
extraJs.addTooltip(
$('<label>')
.attr('for', 'closeXFD-result-delete-disabled')
.text('Delete '),
'Non-admin closure is not appropriate when the result will require action by an administrator (per [[WP:BADNAC]])'
)
),
Line 1,236 ⟶ 1,896:
$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
'class':'closeXFD-deleteResult', 'id':'closeXFD-result-softdel',
'value':'
$('<label>').attr('for', 'closeXFD-result-softdel').text('
),
// Custom
Line 1,243 ⟶ 1,903:
$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
'id':'closeXFD-result-custom', 'value':'custom'}),
$('<label>').attr('for', 'closeXFD-result-custom').text('
)
),
Line 1,250 ⟶ 1,910:
.css({'clear':'both', 'padding-bottom':'1%', 'display':'none'})
.append(
$('<label>').attr('for', 'closeXFD-result-custom-input').text('
$('<input>')
.attr({'type':'text', 'name':'closeXFD-result-custom-input',
Line 1,264 ⟶ 1,924:
.append(
$('<label>').attr({'id':'closeXFD-result-target-label', 'for':'closeXFD-result-target'})
.text('
$('<input>')
.attr({'type':'text', 'name':'closeXFD-result-target', 'id':'closeXFD-result-target'})
.change(function(){
self.inputData.target =
$('#closeXFD-result-target').val().trim()
);
Line 1,285 ⟶ 1,945:
default:
case 'keep':
case '
$('.closeXFD-keepOptions').show();
$('.closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+
Line 1,304 ⟶ 1,964:
$('.closeXFD-keepOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+
'#closeXFD-customOptions, #closeXFD-resultContainer-custom').hide();
$('#closeXFD-result-target-label').text(extraJs.toSentenceCase(v) + '
// default options:
if (
Line 1,310 ⟶ 1,970:
( currentAfter !== 'doRedirectActions' && currentAfter !== 'noAction' )
) {
$('#closeXFD-after-doRedirectActions
.prop('checked', true).change();
}
break;
case '
$('.closeXFD-mergeOptions, #closeXFD-resultContainer-target').show();
$('.closeXFD-keepOptions, .closeXFD-redirectOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+
'#closeXFD-customOptions, #closeXFD-resultContainer-custom').hide();
$('#closeXFD-result-target-label').text('
// default options:
if (
Line 1,340 ⟶ 2,000:
}
break;
case '
case 'soft delete':
$('.closeXFD-deleteOptions').show();
Line 1,354 ⟶ 2,014:
) {
$('#closeXFD-after-' +
(( config.
', #closeXFD-del-deleteredir, #closeXFD-del-unlinkbackl')
.prop('checked', true).change();
Line 1,391 ⟶ 2,051:
.append(
$('<strong>').attr('id', 'closeXFD-rationale-label')
.text( ( self.relisting ) ? '
'
( !multimode ) ? ' (
.addClass('xfdc-dialog-bracketedOption')
.text('copy from above')
Line 1,402 ⟶ 2,062:
if ( a ) {
a = a
.replace('del', '
.replace(/(default|na)/, ' ');
} else {
a = ' ';
}
var t = ( /(
self.inputData.pageActions[p].target + ']]
results += "*'''" + extraJs.toSentenceCase(a) + "''' [[" + p + "]]" + t;
}
Line 1,436 ⟶ 2,096:
.change(),
$('<label>').attr('for', 'closeXFD-rationale-sentence')
.text('
)
);
Line 1,444 ⟶ 2,104:
.css({'clear':'both', 'padding-bottom':'1%'})
.append(
extraJs.addTooltip(
$('<label>').attr('for', 'closeXFD-resultSummary-input').text('Result summary: '),
),
$('<input>')
.attr({
'type':'text', 'name':'closeXFD-resultSummary-input', 'id':'closeXFD-resultSummary-input'
})
.css({"margin-left": "0.5em"})
.change(function(){
self.inputData.result = $(this).val().trim();
Line 1,471 ⟶ 2,134:
.css('max-width', ( multimode ) ? '30%' : '40%')
.append(
$('<strong>').text('
// KeepActions
$('<div>')
Line 1,478 ⟶ 2,141:
.append(
( multimode ) ? $('<strong>')
.text('"
.attr({
'type':'radio',
Line 1,485 ⟶ 2,148:
'value':'doKeepActions'
}),
extraJs.addTooltip(
$(( multimode ) ? '<span>' : '<label>')
.attr('for', 'closeXFD-after-doKeepActions')
.text('Update pages and talk pages '),
'Remove nomination templates, and add old xfd templates to talk pages'
)
),
Line 1,499 ⟶ 2,161:
.append(
( multimode ) ? $('<strong>')
.text('"
'type':'radio',
'name':'closeXFD-after',
Line 1,505 ⟶ 2,167:
'value': 'doRedirectActions'
}),
extraJs.addTooltip(
$(( multimode ) ? '<span>' : '<label>')
.attr('for', 'closeXFD-after-doRedirectActions')
.text('Redirect pages and update talk pages '),
'Redirect nominated pages to the specified target, '+
'and add old xfd templates to talk pages'
)
),
Line 1,519 ⟶ 2,181:
.append(
( multimode ) ? $('<strong>')
.text('"
'type':'radio',
'name':'closeXFD-after',
Line 1,525 ⟶ 2,187:
'value': 'doMergeActions'
}),
extraJs.addTooltip(
$(( multimode ) ? '<span>' : '<label>')
.attr('for', 'closeXFD-after-doMergeActions')
.text('Update pages and talk pages '),
'and add old xfd templates to talk pages'
)
),
Line 1,539 ⟶ 2,201:
.append(
( multimode ) ? $('<strong>')
.text('"
.attr({
'type':'radio',
Line 1,546 ⟶ 2,208:
'value':'doDisambigActions'
}),
extraJs.addTooltip(
$(( multimode ) ? '<span>' : '<label>')
.attr('for', 'closeXFD-after-doDisambigActions')
.text('Update pages and talk pages '),
'Remove nomination templates, and add old xfd templates '+
'to talk pages'
)
),
Line 1,565 ⟶ 2,227:
'value': 'doDeleteActions'
}),
extraJs.addTooltip(
$('<label>')
.attr('for', 'closeXFD-after-doDeleteActions')
.text('Delete pages '),
'Delete nominated pages and (unless otherwise specified) '+
'their talk pages'
)
),
Line 1,582 ⟶ 2,246:
'value':'holdingCell'
}),
extraJs.addTooltip(
$('<label>')
.attr('for', 'closeXFD-after-holdingCell')
.text('
'Replace nomination template with {{Being deleted}}, '+
'and list at the specified holding cell section'
)
),
Line 1,593 ⟶ 2,259:
.hide()
.append(
$('<strong>').text('"
( !config.user.isSysop ) ? '' : extraJs.addTooltip(
$("<span>").text('Delete pages '),
'Delete nominated pages and (unless otherwise specified) their talk pages'
),
( config.xfd.type !== 'tfd' ) ? '' : extraJs.addTooltip(
$("<span>").text( (( config.
)
),
Line 1,618 ⟶ 2,283:
}),
$(( multimode ) ? '<span>' : '<label>')
.attr('for', 'closeXFD-after-noAction').text('
)
)
Line 1,661 ⟶ 2,326:
.hide()
.append(
$('<strong>').text(( multimode ) ? '
.css('display','block'),
// Delete before redirecting
Line 1,680 ⟶ 2,345:
}),
$('<label>').attr('for', 'closeXFD-redir-deleteFirst')
.text('Delete before redirecting')
),
// Rcats
$('<div>').append(
$('<input>').attr({
'type':' 'name':'closeXFD-redir-addRcats', 'id': 'closeXFD-redir-
'disabled': true
}).prop('checked', ( config.xfd.type === 'afd' )),
$('<label>').attr('for', 'closeXFD-redir-rcats').append(
extraJs.addTooltip(
$('<a>')
.attr({ 'href':'https:// 'target':'_blank'
}) .text('Rcats: '), 'Full markup required, e.g. "{{R to section}}". '+
'Multiple rcats can be specified, e.g. "{{R from book}}'+
'{{R to anchor}}". Leave blank if unsure which rcat to use.'
)
),
self.rcatSelector.$element.detach()
)
),
// Merge options:
Line 1,795 ⟶ 2,448:
'checked':'checked'}),
$('<label>').attr('for', 'closeXFD-del-deleteredir')
.text('
),
// Unlink backlinks
Line 1,803 ⟶ 2,456:
'id':'closeXFD-del-unlinkbackl', 'value':'unlinkbackl',
'checked':'checked'}),
$('<label>').attr('for', 'closeXFD-del-unlinkbackl').text('
),
// Delete and redirect
Line 1,812 ⟶ 2,465:
// Shortcut for switching to 'Redirect' and selects 'Delete before redirecting'
$('<a>').attr('id', 'closeXFD-del-deleteAndRedir')
.text('
.click(function(){
$('#closeXFD-result-redir').prop('checked', true).change();
Line 1,927 ⟶ 2,580:
$('<button>')
.attr('id', 'closeXFD-interface-cancel')
.text('
.click(function() {
self.close();
Line 1,937 ⟶ 2,590:
$('<button>')
.attr('id', 'closeXFD-interface-closedisc')
.text('
.click(function() {
if ( multimode ) {
Line 1,947 ⟶ 2,600:
$('<button>')
.attr('id', 'closeXFD-interface-preview')
.text('
.click(function() {
if ( multimode ) {
Line 1,958 ⟶ 2,611:
} else {
$buttons.prepend(
$('<button>').attr('id', 'closeXFD-interface-relistdisc').text('
.click(function() {
self.close();
Line 1,964 ⟶ 2,617:
self.discussion.taskManager.start();
}),
$('<button>').attr('id', 'closeXFD-interface-previewRelist').text('
.click(function() {
self.showPreview(true);
Line 1,981 ⟶ 2,634:
$('.closeXFD-keepOptions').toggle(this.inputData.inPageActions('keep'));
$('.closeXFD-redirectOptions').toggle(this.inputData.inPageActions('redirect'));
$('.closeXFD-mergeOptions').toggle(this.inputData.inPageActions('
$('.closeXFD-disambigOptions').toggle(this.inputData.inPageActions('disambig'));
$('.closeXFD-deleteOptions').toggle(this.inputData.inPageActions('del'));
Line 1,990 ⟶ 2,643:
Dialog.prototype.storeRcatData = function(rcatData) {
this.inputData.rcats = rcatData.join('\n').trim();
$('#closeXFD-redir-rcats').prop('checked', rcatData.length > 0);
};
Line 1,998 ⟶ 2,652:
this.emptyContent();
if ( config.xfd.type !== 'ffd' && config.xfd.type !== 'cfd' && !self.discussion.isBasicMode() ) {
this.rcatSelector =
}
this.inputData = new InputData(this.discussion);
this.addToHeader(this.makeHeader(false, this.relisting));
this.addToBody(this.makePagesList());
Line 2,015 ⟶ 2,669:
this.addToBody(this.makeCloseResult());
this.addToBody(this.makeRationaleBox());
if ( config.xfd.type === 'cfd' || this.discussion.isBasicMode() ) {
this.inputData.after = 'noAction';
} else {
if ( config.xfd.type === 'afd' ) {
this.rcatSelector.
}
this.addToBody(this.makeAfterActions());
Line 2,040 ⟶ 2,694:
if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode() ) {
this.rcatSelector =
}
Line 2,056 ⟶ 2,710:
this.addToBody(this.makeAfterActions(true));
if ( config.xfd.type === 'afd' ) {
this.rcatSelector.
}
this.addToBody(this.makeOptions(true));
Line 2,075 ⟶ 2,729:
preview_wikitext = '{{Relist|1=' + this.inputData.getRelistComment() + '}}';
} else {
preview_wikitext = "
this.inputData.getResult()+
"'''" +
(( this.inputData.getTargetLink() ) ? '
( (this.inputData.getRationale()) || '
' ' +
config.user.sig;
Line 2,090 ⟶ 2,744:
})
.done(function(result) {
var preview_html = result.parse.text['*'];
$('#closeXFD-preview-output').html(preview_html).show().find('a').attr('target', '_blank');
})
Line 2,100 ⟶ 2,754:
.css('color', '#f00')
.append(
'
formatApiError(code, jqxhr)
)
)
Line 2,133 ⟶ 2,785:
// Evaluate standard close
Dialog.prototype.evaluateClose = function(preview) {
var self = this;
var errors = [];
Line 2,148 ⟶ 2,800:
case 'redirect':
case 'soft redirect':
case '
if ( $('#closeXFD-result-target').val().trim() === '' ) {
// Target not specified
Line 2,155 ⟶ 2,807:
if ( !self.inputData.target ) {
// Invalid target
window.alert('Bad ' + self.inputData.result +
' target: the title is invalid.');
errors.push('#closeXFD-resultContainer-target');
Line 2,223 ⟶ 2,875:
// Evaluate multimode close
Dialog.prototype.evaluateMultimodeClose = function(preview) {
var self = this;
var errors = [];
Line 2,251 ⟶ 2,903:
} else if (
self.inputData.pageActions[p] === 'redirect' ||
self.inputData.pageActions[p] === '
) {
// Check target specified
Line 2,258 ⟶ 2,910:
errors.push('#'+targetInputId);
} else if ( !self.inputData.pageActions[p].target ) {
window.alert('Bad ' + self.inputData.pageActions[p].action + 'target for ' +
p +' – the title is invalid.');
errors.push('#'+targetInputId);
Line 2,268 ⟶ 2,920:
if (
config.xfd.type === 'tfd' &&
self.inputData.inPageActions('
!self.inputData.mergeHoldcell
) {
Line 2,381 ⟶ 3,033:
var output = '';
if ( this.speedy ) {
output += '
}
if (
Line 2,387 ⟶ 3,039:
/(?:retarget|redirect|soft redirect)/.test(this.result)
) {
output += '
}
output += ( this.result === 'custom') ? this.customResult : this.result;
Line 2,398 ⟶ 3,050:
if ( this.multimode && p ) {
return this.pageActions[p].target.getPrefixedText();
} else if ( /(?:retarget|redirect|soft redirect|
return this.target && this.target.getPrefixedText();
} else {
Line 2,429 ⟶ 3,081:
if ( self.multimode ) {
for ( var p in self.pageActions ) {
if ( self.pageActions[p] && /(redirect|
output.push(self.pageActions[p].target);
}
Line 2,446 ⟶ 3,098:
if (
self.pageActions[p] &&
self.pageActions[p].action === '
self.pageActions[p].target.getPrefixedText() === target
) {
Line 2,505 ⟶ 3,157:
var multimode = self.inputData.multimode;
var warnings = [];
// Check if closing dicussion early (and it hasn't been relisted )
if ( !self.discussion.isOld && !self.discussion.isRelisted ) {
warnings.push('It has not yet been 7 days since the discussion was listed.');
}
// Check for mass actions when closing:
Line 2,529 ⟶ 3,186:
if ( targetObjectsArray ) {
$.each(targetObjectsArray, function(_i, t) {
if ( t && !
warnings.push( 'Target page [["' + t.getPrefixedText() + '"]] is not in the ' +
config.mw.namespaces[(config.xfd.ns_number[0]).toString()] + ' namespace.');
}
});
Line 2,537 ⟶ 3,194:
//Check namespaces of nominated pages
var wrongNamespacePages = !self.discussion.
return !
});
if ( wrongNamespacePages.length > 0 ) {
warnings.push(
'The following pages are not in the ' +
config.mw.namespaces[(config.xfd.ns_number[0]).toString()] +
' namespace:<ul><li>[[' +
wrongNamespacePages.map(function(p){ return p.
']]</li>'
);
Line 2,557 ⟶ 3,214:
};
// If nominated pages are redirects (at venues other than RfD), the script can't know if this was
// appropriate, where results such as 'Delete' should be applied to the target, or an out-of-process
// redirection, where results such as 'Delete' should be applied to the redirect
/**ResolveRedirects
* @returns {Promise<Boolean>} `true` when completed and okay to continue; rejected if aborted by user
*/
TaskManager.prototype.resolveRedirects = function() {
var self = this;
Line 2,571 ⟶ 3,230:
config.xfd.type === 'rfd' ||
// Basic mode (no pages detected)
// Relisting for other than FfD/TfD
( relisting && !/(ffd|tfd)/.test(config.xfd.type) ) ||
Line 2,580 ⟶ 3,239:
) {
// Nominate pages expected to be redirects, no need to resolve them
return $.Deferred().resolve(true).promise();
}
return API.get( {
action: 'query',
titles: self.discussion.getPageTitles().join('|'),
redirects: 1,
prop: 'info',
inprop: 'talkid'
} )
.then( function(result) {
if ( result.query && result.query.redirects ) {
var redirections = result.query.redirects.map(function(redirect) {
return '<li>[[' + redirect.from + ']] → [[' + redirect.to + ']]</li>';
});
var redirects_msg = "The following nominated pages are redirects to other pages:<ul>" +
redirections.join("") + "</ul>";
return multiButtonConfirm({
message: redirects_msg,
actions: [
{ label: 'Cancel', flags: 'safe' },
{ label: 'Use redirects', action: 'reject' },
{ label: 'Use targets', action: 'accept', flags: 'progressive' }
],
size: 'medium'
})
.then(function(action) {
if ( action === 'accept' ) {
// Update discussion pages to use targets (for the ones that are redirects)
var
self.discussion.pages = self.discussion.pages.map(function(page) {
return redirect.from === page.getPrefixedText();
});
if ( !redirection ) {
return page;
}
var target = apiResponsePages.find(function(p) {
return p.title === redirection.to;
});
var targetTitle = mw.Title.newFromText(target.title);
setExistence(targetTitle, target.pageid > 0);
setExistence(targetTitle.getTalkPage(), target.talkid > 0);
return targetTitle;
});
} else if ( action === 'reject' ) {
//
} else {
// Abort closing, reset discussion links
self.discussion.showLinks();
return $.Deferred().reject('Cancelled by user');
}
});
} else {
// No redirects present, just get on with it
return true;
}
});
};
Line 2,682 ⟶ 3,325:
self.afdLogEditIndex = config.track.afdLogEdit.push($.Deferred()) - 1;
// Notify user
self.discussion.setStatus('
'need to be completed)');
break;
case 'mfd':
Line 2,691 ⟶ 3,334:
Array.prototype.push.apply(self.tasks, [
new Task('updateOldLogPage', self.discussion),
new Task('updateNewLogPage', self.discussion),
( !self.discussion.isBasicMode() && self.discussion.pages.length > 1 )
? new Task('updateNomTemplates', self.discussion)
: ''
]
.filter(function(v){ return !!v; })
);
break;
default: // ffd, tfd
Line 2,698 ⟶ 3,346:
new Task('updateOldLogPage', self.discussion),
new Task('updateNewLogPage', self.discussion),
( !self.discussion.
]
.filter(function(v){ return !!v; })
Line 2,718 ⟶ 3,366:
self.inputData.inPageActions('keep') ||
self.inputData.inPageActions('redirect') ||
( self.inputData.inPageActions('
self.inputData.inPageActions('disambig')
) {
self.tasks.push(new Task('addOldXfd',
self.discussion, self.inputData.getPages(
'keep', 'redirect', ( /(afd|mfd)/.test(config.xfd.type) ) ? '
)
));
Line 2,738 ⟶ 3,386:
}
// For merge (not holding cell) results:
if ( config.xfd.type !== 'tfd' && self.inputData.inPageActions('
self.tasks.push(new Task('addMergeTemplates',
self.discussion, self.inputData.getPages('
}
// For disambiguate results:
Line 2,763 ⟶ 3,411:
// For TfD holding cell results
if ( config.xfd.type === 'tfd' ) {
var doHcMerge = self.inputData.inPageActions('
var doHcDelete = self.inputData.inPageActions('del') && self.inputData.useHoldingCell;
if ( doHcMerge && doHcDelete ) {
Line 2,769 ⟶ 3,417:
Array.prototype.push.apply(self.tasks, [
new Task('addBeingDeleted', self.discussion,
self.inputData.getPages('
new Task('addToHoldingCell', self.discussion,
self.inputData.getPages('
( self.inputData.dontdeletetalk ) ? null : new Task('tagTalkWithSpeedy',
self.discussion, self.inputData.getPages('
]
.filter(function(v){ return !!v; })
Line 2,780 ⟶ 3,428:
// Only 'merge' (via holding cell) results
Array.prototype.push.apply(self.tasks, [
new Task('addBeingDeleted', self.discussion, self.inputData.getPages('
new Task('addToHoldingCell', self.discussion, self.inputData.getPages('
]
);
Line 2,884 ⟶ 3,532:
$.each(self.tasks.slice(1), function(_i, t) {
if ( t.start ) {
t.start(); // TODO: set task status, errors, and warning here, from the returned promise
} else { // TODO: Investigate what the point of this is.
self.tasks[_i+1].start();
}
Line 2,903 ⟶ 3,551:
$.when( config.track.afdLogEdit[self.afdLogEditIndex-1] )
.then( function() {
self.discussion.setStatus('
self.tasks[0].start();
} );
Line 2,914 ⟶ 3,562:
var self = this;
var sanityCheckWarnings = self.makeSanityCheckWarnings();
$.when(
( sanityCheckWarnings )
? multiButtonConfirm({
title: 'Warning',
message: sanityCheckWarnings,
actions: [
{ label: 'Cancel', flags: 'safe' },
{ label: 'Continue', action: 'proceed', flags: 'progressive' }
],
size: 'large'
})
: $.Deferred().resolve('proceed')
)
.then(function(action) {
if ( action !== 'proceed' ) {
// Reset discussion links
Line 2,921 ⟶ 3,582:
return;
}
// Resolve redirects
})
.then(function(okayToProceed) {
if ( !okayToProceed ) {
return;
}
// ... then initialise
var isRelisting = !self.inputData.result;
if ( isRelisting ) {
self.initialiseForRelist();
} else if ( self.inputData.multimode ) {
} else {
}
});
};
Line 2,956 ⟶ 3,612:
TaskManager.prototype.makeTaskNote = function(task) {
var $notice = $('<p>')
.addClass('xfdc-task-' + task.status)
.addClass(task.name)
Line 3,035 ⟶ 3,691:
switch ( taskname ) {
case 'closeDisc':
this.description = '
break;
case 'addOldXfd':
this.description = '
break;
case 'removeNomTemplates':
case 'addMergeTemplates':
case 'disambiguate':
this.description = '
break;
case 'peformDeletion':
this.description = '
break;
case 'addBeingDeleted':
this.description = '
break;
case 'addToHoldingCell':
this.description = '
break;
case 'deleteTalk':
this.description = '
break;
case 'tagTalkWithSpeedy':
this.description = '
' for speedy deletion';
break;
case 'deleteRedirects':
this.description = '
break;
case 'unlinkBacklinks':
this.description = '
break;
case 'redirect':
if ( discussion.dialog.inputData.deleteFirst ) {
this.description = '
'
} else if ( config.xfd.type === 'rfd' ) {
this.description = '
} else {
this.description = '
'
}
break;
case 'removeCircularLinks':
this.description = 'Unlinking circular links on redirect target';
break;
case 'getRelistInfo':
this.description = '
break;
case 'updateDiscussion':
this.description = '
break;
case 'updateOldLogPage':
this.description = '
break;
case 'updateNewLogPage':
this.description = '
break;
case 'updateNomTemplates':
this.description = '
(( plural ) ? 's' : '');
break;
Line 3,113 ⟶ 3,769:
var self = this;
this.status = s;
this.discussion.taskManager.updateTaskNotices(self);
};
Task.prototype.updateStatus = function() {
var self = this;
this.discussion.taskManager.updateTaskNotices(self);
};
Line 3,120 ⟶ 3,780:
// Not yet started:
case 'waiting':
return '
// In progress:
case 'started':
$('<img>').attr({
'
'40px-Ajax-loader%282%29.gif',
'
'height':'5'
})
);
if ( self.showTrackingProgress ) {
var counts = this.tracking[self.showTrackingProgress];
$msg.append(
$('<span>')
.css('font-size', '88%')
.append(
' (' +
(counts.success + counts.skipped) +
' / ' +
counts.total +
')'
)
);
}
return $msg;
// Finished:
case 'done':
Line 3,137 ⟶ 3,814:
self.discussion.taskManager.dfd.initialTask.resolve();
}
return '
case 'aborted':
case 'failed':
case 'skipped':
return extraJs.toSentenceCase(self.status) + '.';
default:
// unknown
Line 3,188 ⟶ 3,863:
};
};
Task.prototype.track = function(key, success, amount) {
if ( success ) {
this.tracking[key].success+
} else {
this.tracking[key].skipped+
}
if ( key === this.showTrackingProgress ) {
this.updateStatus();
}
if ( this.tracking[key].skipped === this.tracking[key].total ) {
this.tracking[key].dfd.reject();
Line 3,203 ⟶ 3,883:
Task.prototype.start = function() {
return this.doTask[this.name](this);
};
// Code to actually do the tasks
Task.prototype.doTask = {};
// ---------- Closing tasks -------------
// Close discussion
Task.prototype.doTask.closeDisc = function(self) {
// Notify task is started -- current (to-be-deprecated) method
self.setStatus('started');
// Get nomination page content and remove {Closing} etc templates if present
return API.get( {
action: 'query',
titles: self.discussion.nomPage,
prop: 'revisions',
rvprop: 'content|timestamp',
rvsection: self.discussion.sectionNumber,
indexpageids: 1
} )
.then( function(response) {
var id = response.query.pageids;
return response.query.pages[ id ].revisions[ 0 ];
} )
.then( function(revision) {
var contents =
var lastEditTime = revision.timestamp;
if ( contents.includes(config.xfd.wikitext.alreadyClosed) ) {
return $.Deferred().reject('aborted', null, 'Discussion already closed (reload page to see '+
'
}
/* Start-time check only possible for AFDs/MFDs. Other venues have multiple discussions on
* the same page, which would give false-positives when a different section was edited.
*/
if ( config.xfd.type === 'afd' || config.xfd.type === 'mfd' ) {
var editedSinceScriptStarted = config.startTime < new Date(lastEditTime);
if ( editedSinceScriptStarted ) {
return $.Deferred().reject('aborted: edit conflict detected');
}
}
var section_heading = contents.slice(0, contents.indexOf('\n'));
var decodeHtml = function(t) {
return $('<div>').html(t).text();
};
var plain_section_heading = decodeHtml( section_heading
.replace(/(?:^\s*=*\s*|\s*=*\s*$)/g, '') // remove heading markup
.replace(/\[\[\:?(?:[^\]]+\|)?([^\]]+)\]\]/g, '$1') // replace link markup with link text
.replace(/{{\s*[Tt]l[a-z]?\s*\|\s*([^}]+)}}/g, '{{$1}}') // replace tl templates
.replace(/s*}}/, '}}') // remove any extra spaces after replacing tl templates
.replace(/\s{2,}/g, ' ') // collapse multiple spaces into a single space
.trim()
);
var isCorrectSection = plain_section_heading === self.discussion.sectionHeader;
if ( !isCorrectSection ) {
return $.Deferred().reject('aborted: possible edit conflict (found section heading `' +
plain_section_heading + '`)');
}
var xfd_close_top = config.xfd.wikitext.closeTop
.replace(/__RESULT__/, self.inputData.getResult() || ' ')
.replace(/__TO_TARGET__/, ( self.inputData.getTarget() ) ? ' to ' +
self.inputData.getTargetLink() : '')
.replace(/__RATIONALE__/, ( self.inputData.getRationale() || '.'))
.replace(/__SIG__/, config.user.sig);
var section_contents = contents.slice(contents.indexOf('\n')+1)
.replace(
/({{closing}}|{{AfDh}}|{{AfDb}}|\{\{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD\|.?\}\}|<noinclude>\[\[Category:Relisted AfD debates\|.*?\]\](\[\[Category:AfD debates relisted 3 or more times|.*?\]\])?<\/noinclude>)/gi,
'');
var updated_top = ( config.xfd.type === 'afd' || config.xfd.type === 'mfd' ) ?
xfd_close_top + '\n' + section_heading :
section_heading + '\n' + xfd_close_top;
return $.Deferred().resolve(updated_section, lastEditTime);
} )
.then( function(updated_section, lastEditTime) {
return API.postWithToken( 'csrf', {
action: 'edit',
title: self.discussion.nomPage,
section: self.discussion.sectionNumber,
text: updated_section,
summary: '/* ' + self.discussion.sectionHeader + ' */ Closed as ' +
self.inputData.getResult() + config.script.advert,
basetimestamp: lastEditTime
} );
} )
.then(
self.setStatus('done'); // Current (to-be-deprecated) method of setting status
return 'done'; // Future method of setting status
},
function(code, jqxhr, abortReason) {
var message = [
'Could not edit page ',
extraJs.makeLink(self.discussion.nomPage),
'; could not close discussion'
self.addApiError(code, jqxhr, message);
var abortMessage = ( code.indexOf("aborted") === 0 && abortReason ) || '';
self.discussion.taskManager.abortTasks(abortMessage);
return $.Deferred().reject(code, jqxhr, message);
}
);
};
Line 3,295 ⟶ 4,002:
// Notify task is started
self.setStatus('started');
// Make wikitext of olf xfd template (non-AFD)
var makeOldxfdWikitext = function(altpage) {
var result = config.xfd.wikitext.oldXfd
.replace(/__DATE__/, self.discussion.nomDate)
.replace(/__SECTION__/, self.discussion.sectionHeader)
Line 3,340 ⟶ 4,023:
var titleObject = mw.Title.newFromText(pageTitle);
var PAGENAME = titleObject.getMain();
var SUBJECTPAGENAME = config
PAGENAME;
var oldafdmulti = '{{Old AfD multi';
Line 3,413 ⟶ 4,096:
var oldTfdDate = oldTfdParams.date;
var oldTfdResult = oldTfdParams.result || 'keep';
var oldTfdLink = "{{subst:#ifexist:
( oldTfdParams.link || "{{subst:Date|"+oldTfdParams.date+"|ymd}}" )+
"|Wikipedia:Templates for deletion/Log/" +
( oldTfdParams.link || "{{subst:Date|"+oldTfdParams.date+"|ymd}}" )+
"#" + ( oldTfdParams['1'] || oldTfdParams.disc || "
"|Wikipedia:Templates for discussion/Log/"+
( oldTfdParams.link || "{{subst:Date|"+oldTfdParams.date+"|ymd}}" )+
"#" + ( oldTfdParams['1'] || oldTfdParams.disc || "
"}}";
oldafdmulti += " |date" + count + "=" + oldTfdDate +
Line 3,438 ⟶ 4,121:
var oldFfdDate = oldFfdParams.date || '';
var oldFfdResult = oldFfdParams.result || 'keep';
var oldFfdLink = "{{subst:#ifexist:
"{{subst:#iferror:{{subst:#time:|"+oldFfdParams.date+"}}|"+oldFfdParams.date+
"|{{subst:#time:Y F j|"+oldFfdParams.date+"}}}}"+
"|
oldFfdParams.date+"}}|"+oldFfdParams.date+"|{{#time:Y F j|"+oldFfdParams.date+
"}}}}#File:" + ( oldFfdParams.page || PAGENAME ) +
"|{{subst:#ifexist:
oldFfdParams.date+"}}|"+oldFfdParams.date+"|{{subst:#time:Y F j|"+
oldFfdParams.date+"}}}}"+
"|
oldFfdParams.date+"|{{subst:#time:Y F j|"+oldFfdParams.date+"}}}}#" +
( oldFfdParams.page || SUBJECTPAGENAME )+
"|
oldFfdParams.date+"|{{subst:#time:Y F j|"+oldFfdParams.date+"}}}}#" +
( oldFfdParams.page || SUBJECTPAGENAME )+
Line 3,471 ⟶ 4,154:
var oldMfdDate = oldMfdParams.date || '';
var oldMfdResult = oldMfdParams.result || 'keep';
var oldMfdLink = "
( oldMfdParams.votepage || oldMfdParams.title ||
oldMfdParams.page || SUBJECTPAGENAME );
Line 3,495 ⟶ 4,178:
oldRfdLink = oldRfdParams.rawlink.slice(2, oldRfdParams.rawlink.indexOf('|'));
} else {
oldRfdLink = "
( oldRfdParams.page || oldRfdParams.date + "#" + SUBJECTPAGENAME );
}
Line 3,531 ⟶ 4,214:
oldafdmulti += ' |date'+c + '=' + self.discussion.nomDate +
' |result'+c+ "='''" + currentResult + "'''" + currentNompageParamAndValue + '}}';
var newtext;
if ( oldAfdPatt.test(wikitext) ) {
// Override the existing oldafdmulti
Line 3,540 ⟶ 4,224:
return newtext;
};
// Get talk pages
var talkTitles = self.discussion.getTalkTitles(self.pages);
if ( talkTitles.length === 0 ) {
self.addWarning('none found');
self.setStatus('done');
return;
}
// get talk page redirect status from api
self.setupTracking('processed', talkTitles.length);
// Function to transform a simplified API page object into edit parameters for API.editWithRetry
var transform = function(page) {
// Check there's a corresponding nominated page
var pageObj = self.discussion.getPageByTalkTitle(page.title);
if ( !pageObj ) {
return $.Deferred().reject("Unexpected title");
}
// Check corresponding page exists
if ( !pageObj.exists() ) {
return $.Deferred().reject("Subject page does not exist");
}
var baseEditParams = {
section: '0',
summary: 'Old ' + config.xfd.type.toUpperCase() + ' – ' + self.discussion.nomDate +
': ' + self.inputData.getResult() + config.script.advert,
};
// Check redirect status
if ( config.xfd.type !== 'afd' && page.redirect ) {
// Is a redirect...
if ( config.xfd.type === 'rfd' ) {
// for RFD, ask what to do
return OO.ui.confirm('"' + page.title + '" is currently a redirect. Okay ' +
'to replace with Old RFD template?')
.then( function ( confirmed ) {
if (!confirmed) {
return $.Deferred().reject('skipped');
}
return $.extend(baseEditParams, {
text: makeOldxfdWikitext(),
redirect: false
});
} );
} else if ( config.xfd.type === 'mfd' ) {
// For MFD, edit the redirect's target page, using the altpage parameter
return $.extend(baseEditParams, {
prependtext: makeOldxfdWikitext(pageObj.getPrefixedText()),
redirect: true
});
} else {
// For other venues, append rather than prepend old xfd template
return $.extend(baseEditParams, {
appendtext: '\n' + makeOldxfdWikitext(),
redirect: false
}
}
// Not a redirect. Attempt to find and consolidate existing banners
var oldwikitext = page.missing ? '' : page.content;
return $.extend(baseEditParams, {
text: makeNewWikitext(oldwikitext, page.title),
redirect: false,
});
}
};
API.editWithRetry(
talkTitles,
{rvsection: '0'},
transform,
function() { self.track('processed', true); },
function(code, error, title) {
case "Unexpected title":
self.addError([
'API query result included unexpected talk page title ',
extraJs.makeLink(title),
'; this talk page will not be edited'
]);
self.track('processed', false); case "Subject page does not exist":
self.addError([
'The subject page for ',
extraJs.makeLink(title),
' does not exist; this talk page will not be edited'
]);
self.track('processed', false);
case "skipped":
self.addWarning([
extraJs.makeLink(title),
' skipped'
]);
self.track('processed', true);
break;
default:
self.addApiError(code, error, [
'Could not edit talk page ',
extraJs.makeLink(title)
]);
}
}
).fail( function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
self.addApiError(code, jqxhr, 'Could not read contents of talk page' +
( talkTitles.length > 1 ) ? 's' : '');
self.setStatus('failed');
} // other errors handled above
} );
};
Task.prototype.doTask.removeNomTemplates = function(self, mergeWikitext) {
// Notify task is started
self.setStatus('started');
// Get page titles
var pageTitles = self.discussion.getPageTitles(self.pages, {'moduledocs': true});
if ( pageTitles.length === 0 ) {
self.addWarning('
self.setStatus('
return;
}
Line 3,739 ⟶ 4,359:
);
// Function to transform a simplified API page object into edit parameters for API.editWithRetry
var transform = function(page) {
// Check there's a corresponding nominated page
var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true});
if ( !pageObj ) {
return $.Deferred().reject("unexpectedTitle");
}
// Check corresponding page exists
if ( !pageObj.exists() ) {
return $.Deferred().reject("doesNotExist");
}
var oldWikitext = page.content;
// Start building updated wikitext
var updatedWikitext = '';
// For merging, unless the page is itself a merge target, prepend the mergeWikitext
if (
mergeWikitext != null &&
$.inArray(page.title, self.inputData.getTargetsArray('merge')) === -1
) {
updatedWikitext = mergeWikitext[page.title];
}
if ( config.xfd.hasNomTemplate(oldWikitext) ) {
try {
// Remove nom template
updatedWikitext += config.xfd.removeNomTemplate(oldWikitext);
} catch(e){
// Error if multiple nom templates found
return $.Deferred().reject("couldNotUpdate", e);
}
} else {
// Nomination template not found
if ( updatedWikitext === '' ) {
// Skip - nothing to change
return $.Deferred().reject("nominationTemplateNotFound");
} else {
// Show warning
self.addWarning([
'Nomination template not found on page ',
extraJs.makeLink(page.title)
]);
// Keep going - can still prepend wikitext for merging
updatedWikitext += oldWikitext;
}
}
return {
text: updatedWikitext,
summary: config.xfd.type.toUpperCase() + ' closed as ' +
self.inputData.getResult() + config.script.advert
};
};
API.editWithRetry(
pageTitles,
null,
transform,
function() { self.track('edit', true); },
function(code, error, title) {
switch(code) {
case "unexpectedTitle":
self.addError([
'API query result included unexpected title ',
extraJs.makeLink(title),
'; this talk page will not be edited'
]);
self.track('edit', false);
break;
case "doesNotExist":
self.addError([
extraJs.makeLink(title),
' does not exist, and will not be edited'
]);
self.track('processed', false);
break;
case "nominationTemplateNotFound":
self.addWarning([
'Nomination template not found on page ',
extraJs.makeLink(title)
]);
self.track('edit', false);
break;
case "couldNotUpdate":
self.addError([
'Could not update ',
extraJs.makeLink(title),
': ',
error.message
]);
self.track('edit', false);
break;
default:
self.addApiError(code, error, [
'Could not edit page ',
extraJs.makeLink(title)
]);
self.track('edit', false);
}
}
).fail( function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
self.addApiError(code, jqxhr, 'Could not read contents of nominated page' +
( pageTitles.length > 1 ) ? 's' : '');
self.setStatus('failed');
} // other errors handled above
} );
};
Line 3,761 ⟶ 4,473:
// Edit function
var editTargetTalk = function(pageTitle, prependWikitext, isRetry) {
API.postWithToken( 'csrf', {
action: 'edit',
title: pageTitle,
prependtext: prependWikitext,
summary: '[[:' + self.discussion.getNomPageLink() + ']]
self.inputData.getResult() + config.script.advert
} )
Line 3,773 ⟶ 4,485:
})
.fail( function(code, jqxhr) {
if (!isRetry) {
return editTargetTalk(pageTitle, prependWikitext, true);
}
self.track('editTargetTalk', false);
self.addApiError(code, jqxhr, [
Line 3,782 ⟶ 4,497:
// Get targets and their talk pages
var targets = extraJs.uniqueArray(self.inputData.getTargetsArray('
self.setupTracking('alldone', 2);
Line 3,795 ⟶ 4,510:
var debate = self.discussion.getNomSubpage();
var today = new Date();
var curdate = today.getUTCDate().toString() + ' ' + config
today.getUTCFullYear().toString();
Line 3,816 ⟶ 4,531:
.replace(/__DEBATE__/, debate)
.replace(/__DATE__/, curdate)
.replace(/__TARGETTALK__/,
// Make 'merge from' template for the target's talk page
Line 3,840 ⟶ 4,555:
} else {
// Edit target talkpage
editTargetTalk(
}
}
Line 3,850 ⟶ 4,565:
Task.prototype.doTask.disambiguate = function(self) {
// Notify task is started
self.setStatus('started');
var pageTitles = self.discussion.getPageTitles(self.pages);
if ( pageTitles.length === 0 ) {
self.addWarning('none found');
self.setStatus('failed');
return;
}
self.setupTracking('edit', pageTitles.length);
// Function to transform a simplified API page object into edit parameters for API.editWithRetry
var transform = function(page) {
// Check there's a corresponding nominated page
var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true});
if ( !pageObj ) {
return $.Deferred().reject("unexpectedTitle");
}
// Check corresponding page exists
if ( !pageObj.exists() ) {
return $.Deferred().reject("doesNotExist");
}
var oldWikitext = page.content;
var updatedWikitext = '';
if ( config.xfd.regex.fullNomTemplate.test(oldWikitext) ) {
updatedWikitext = oldWikitext.replace(config.xfd.regex.fullNomTemplate, '').trim();
} else {
self.addWarning([
'Nomination template not found on page ',
extraJs.makeLink(page.title)
]);
updatedWikitext = oldWikitext.replace(/^#REDIRECT/mi, '*');
}
if ( !/(?:disambiguation|disambig|dab|Mil-unit-dis|Numberdis)[^{]*}}/i.test(updatedWikitext) ) {
updatedWikitext += '\n{{Disambiguation cleanup|{{subst:DATE}}}}';
updatedWikitext.trim();
}
return {
text: updatedWikitext,
summary: config.xfd.type.toUpperCase() + '
self.inputData.getResult() + config.script.advert
}
};
API.editWithRetry(
pageTitles,
null,
transform,
function() { self.track('edit', true); },
function(code, error, title) {
switch(code) {
case "unexpectedTitle":
self.addError([
'API query result included unexpected title ',
extraJs.makeLink(
'; this talk page will not be edited'
]);
self.track('edit', false);
case "doesNotExist":
self.addError([
extraJs.makeLink(title),
' does not exist, and will not be edited'
]);
self.track('processed', false);
break;
default:
self.addApiError(code, error, [
'Could not edit page ',
extraJs.makeLink(title)
]);
self.track('edit', false);
}
}
).fail( function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
self.addApiError(code, jqxhr, 'Could not read contents of nominated page' +
self.setStatus('failed');
} // other errors handled above
} );
};
Line 3,954 ⟶ 4,658:
// Delete with the api
var apiDeletePage = function(pageTitle, isRetry) {
API.postWithToken( 'csrf', {
action: 'delete',
Line 3,964 ⟶ 4,668:
} )
.fail( function(code, jqxhr) {
if (!isRetry) {
return apiDeletePage(pageTitle, true);
}
self.track('del', false);
self.addApiError(code, jqxhr, [
'Could not delete page ',
extraJs.makeLink(pageTitle)
]);
Line 3,976 ⟶ 4,683:
// For each page, check it exists, then delete with api or add warning
for ( var i=0; i<pages.length; i++ ) {
if ( pages[i].exists() ) {
apiDeletePage(pages[i].
} else {
self.addWarning([
extraJs.makeLink(pages[i].
' skipped: does not exist (may have already been deleted by others)'
]);
self.track('del', false);
Line 3,996 ⟶ 4,703:
var mergeTargets = [];
var mergeTitles = [];
if ( self.inputData.inPageActions('
mergeTargets = self.inputData.getTargetsArray('
mergeTitles = self.discussion.getPageTitles(
(self.inputData.multimode ) ? self.inputData.getPages('
);
}
var templateTitles = self.discussion.getPageTitles(self.pages, {'moduledocs': true});
self.setupTracking('edit', templateTitles.length);
// Function to transform a simplified API page object into edit parameters for API.editWithRetry
var transform = function(page) {
// Check there's a corresponding nominated page
var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true});
if ( !pageObj ) {
return $.Deferred().reject("unexpectedTitle");
}
// Check corresponding page exists
if ( !pageObj.exists() ) {
return $.Deferred().reject("doesNotExist");
// Replace {Template for discussion/dated} or {Tfm/dated} (which
// may or may not be noincluded)
var isModule = ( page.title.indexOf('Module:') === 0 );
var inclusionTag = ( isModule ) ? 'includeonly' : 'noinclude';
var oldWikitext = page.content;
var updatedWikitext = '';
var editsum = '';
if ( $.inArray(page.title, mergeTargets) !== -1 ) {
// If this is a merge target, don't add anything - just remove nom template
updatedWikitext =
editsum = '[[' + self.discussion.getNomPageLink() + ']] closed as ' +
self.inputData.getResult() + config.script.advert;
} else if ( self.inputData.holdcell === 'ready' ) {
// If holding cell section is 'ready for deletion', tag for speedy deletion
updatedWikitext = '<
self.discussion.getNomPageLink() + '}}</
editsum = '[[WP:G6|G6]]
self.discussion.getNomPageLink() + ']]' + config.script.advert;
} else {
// Add Being deleted template, remove nom template
updatedWikitext = '<
mw.util.wikiUrlencode(self.discussion.sectionHeader);
if ( $.inArray(
// If being merged, set merge parameter
updatedWikitext += '|merge=' + self.inputData.getTarget(
}
updatedWikitext += '}}</
editsum = 'Per [[' + self.discussion.getNomPageLink() + ']]
'added {{being deleted}}
}
} catch(e){
// Error if multiple nom templates found
return $.Deferred().reject("couldNotUpdate", e);
}
return {
text: updatedWikitext,
summary: editsum
};
};
API.editWithRetry(
templateTitles,
null,
transform,
function() { self.track('edit', true); },
function(code, error, title) {
switch(code) {
case "unexpectedTitle":
self.addError([
'API query result included unexpected title ',
extraJs.makeLink(title),
'; this talk page will not be edited'
]);
self.track('edit', false);
break;
case "doesNotExist":
self.
extraJs.makeLink(title),
' does not exist, and will not be edited'
]);
self.track('processed', false);
break;
case "couldNotUpdate":
self.addError([
'Could not update ',
extraJs.makeLink(title),
': ',
error.message
]);
self.track('edit', false);
break;
default:
self.addApiError(code, error, [
'Could not edit page ',
extraJs.makeLink(title)
]);
self.track('edit', false);
}
}
).fail( function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
self.addApiError(code, jqxhr, 'Could not read contents of nominated page' +
( templateTitles.length > 1 ) ? 's' : '');
self.setStatus('failed');
} // other errors handled above
} );
};
Line 4,112 ⟶ 4,821:
self.setStatus('started');
var holdCellSections = [
self.inputData.holdcell || null,
self.inputData.mergeHoldcell || null
Line 4,119 ⟶ 4,828:
.map(function(v){ return config.xfd.holdingCellSectionNumber[v]; });
self.setupTracking('
// Function to
var
// Get page contents, make all headings level 3 so sections can be counted
var updatedContent = page.content;
var
var sectionsArray = p_contents.split('===');
var isMergeSection = (4 <=
var listingPages;
Line 4,153 ⟶ 4,844:
listingPages = self.discussion.pages;
} else if ( isMergeSection ) {
listingPages = self.inputData.getPages('
} else {
listingPages = self.inputData.getPages('del');
Line 4,159 ⟶ 4,850:
var tfdlTemplates = '';
//Check namespace and existance
if ( !
self.addError([
extraJs.makeLink(
'
' namespace, and will not be listed at the holding cell'
]);
} else if ( !
self.addError([
extraJs.makeLink(
' does not exist, and will not be listed at the holding cell'
]);
} else {
tfdlTemplates += '*{{tfdl|' +
'|' + self.discussion.nomDate +
'|section=' + self.discussion.sectionHeader + ((
(( listingPage.getNamespaceId() === 828 ) ? '|ns=Module' : '') +
'}}\n';
}
});
if ( tfdlTemplates === '' ) {
// If all don't exist or are in wrong namespace, then there's nothing to do
self.track('processed', false);
}
// Make new section wikitext
var heading = sectionsArray[(
var contents = sectionsArray[(
// Merge subsections have level-4 headings
var headingLevel = isMergeSection ? '====' : '===';
var oldSectionWikitext = headingLevel + heading + headingLevel + contents;
// Remove "* ''None currently''" except if inside a <!--html comment-->
contents = contents.replace(/\n*^\*\s*''None currently''\s*$(?![^<]*?-->)/gim, '');
var newSectionWikitext = headingLevel + heading + headingLevel + '\n' +
contents.trim() + '\n' + tfdlTemplates;
updatedContent = updatedContent.replace(oldSectionWikitext, newSectionWikitext);
});
if (updatedContent === page.content) {
return $.Deferred().reject("noChangesMade");
}
return {
text: updatedContent,
summary: 'Listing template(s) per [[:' + self.discussion.getNomPageLink() + ']]' +
config.script.advert
};
};
API.editWithRetry(
[config.xfd.subpagePath + 'Holding cell'],
null,
transform,
function() { self.track('edit', true); },
function(code, error, title) {
switch(code) {
case "noChangesMade":
self.addError([
'Did not find any changes to make to the holding cell'
]);
self.track('edit', false);
break;
default:
self.addApiError(code, error, [
'Could not add templates to holding cell'
]);
self.track('edit', false);
}
}
).fail( function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
self.addApiError(code, jqxhr, 'Could not read contents of holding cell');
self.setStatus('failed');
} // other errors handled above
} );
};
Task.prototype.doTask.deleteTalk = function(self) {
// Notify task is started
self.setStatus('started');
// Get talk pages
var talkTitles = self.discussion.getTalkTitles(self.pages);
if ( talkTitles.length === 0 ) {
self.addWarning('none found');
self.setStatus('skipped');
return;
}
self.setupTracking('del', talkTitles.length);
// Delete with the api
API.postWithToken( 'csrf', {
action: 'delete',
title:
reason: '[[WP:CSD#G8|G8]]:
} )
.done( function() {
Line 4,244 ⟶ 4,955:
} )
.fail( function(code, jqxhr) {
if (!isRetry) {
return apiDeleteTalkPage(talkTitle, true);
}
self.track('del', false);
self.addApiError(code, jqxhr, [
'Could not delete page ',
extraJs.makeLink(
]);
} );
};
//For each talk page, check that it exists and is eligible for a G8 speedy, then delete it
talkTitles.forEach( function(talkTitle) {
var subjectPage = self.discussion.getPageByTalkTitle(talkTitle);
var isUserTalkBasePage = ( subjectPage.getNamespaceId() === 2 ) && ( !talkTitle.includes('/') );
if ( !subjectPage.getTalkPage().exists() ) {
self.addWarning([
extraJs.makeLink(talkTitle),
' skipped: does not exist (may have already been deleted by others)'
]);
self.track('del', false);
return;
}
if ( isUserTalkBasePage ) {
self.addWarning([
extraJs.makeLink(talkTitle),
' skipped: base user talk page (not eligible for G8 speedy deletion)'
]);
self.track('del', false);
return;
}
// Delete with the api
apiDeleteTalkPage(talkTitle);
});
};
Line 4,281 ⟶ 4,997:
// Notify task is started
self.setStatus('started');
// Get talk pages
var talkTitles = self.discussion.getTalkTitles(self.pages);
if ( talkTitles.length === 0 ) {
self.addWarning('none found');
self.setStatus('skipped');
return;
}
// get talk page redirect status from api
self.setupTracking('tag', talkTitles.length);
var apiTagPage = function(pageTitle, isRetry) {
API.postWithToken( 'csrf', {
action: 'edit',
title: pageTitle,
prependtext: '{{Db-talk}}\n',
summary: '[[WP:G8|G8]]
self.discussion.getNomPageLink() + ']]' + config.script.advert
} )
Line 4,294 ⟶ 5,020:
} )
.fail( function(code, jqxhr) {
if (!isRetry) {
return apiTagPage(pageTitle, true);
}
self.track('tag', false);
self.addApiError(code, jqxhr, [
'Could not tag page ',
extraJs.makeLink(pageTitle),
' for speedy deletion'
]);
} );
};
//For each talk page, check that it exists, then tag it
var subjectPage = self.discussion.getPageByTalkTitle(talkTitle);
if ( !subjectPage.getTalkPage().exists() ) {
self.addWarning([
extraJs.makeLink(talkTitle),
' skipped: does not exist (may have already been deleted by others)'
]);
self.track('tag', false);
}
apiTagPage(talkTitle);
});
};
Line 4,339 ⟶ 5,058:
// Delete redirect with the api
var apiDeleteRedir = function(
API.postWithToken( 'csrf', {
action: 'delete',
title: pageTitle,
reason: '
self.inputData.getResult() + config.script.advert
} )
Line 4,350 ⟶ 5,069:
} )
.fail( function(code, jqxhr) {
if (!isRetry) {
return apiDeleteRedir(pageTitle, true);
}
self.track('delRedir', false);
self.addApiError(code, jqxhr, [
'Could not delete redirect ',
extraJs.makeLink(pageTitle)
]);
Line 4,359 ⟶ 5,081:
// Delete redirect talkpage with the api
var apiDeleteRedirTalk = function(
API.postWithToken( 'csrf', {
action: 'delete',
pageid: talkpageid,
reason: '[[WP:CSD#G8|G8]]:
} )
.done( function() {
Line 4,369 ⟶ 5,091:
} )
.fail( function(code, jqxhr) {
if (!isRetry) {
return apiDeleteRedirTalk(talkpageid, true);
}
self.track('delRedirTalk', false);
self.addApiError(code, jqxhr, [
'Could not delete ',
$('<a>').attr({
'href':'https://
'target':'_blank'
}).text('
]);
} );
Line 4,390 ⟶ 5,115:
);
// Delete each redirect
redirectTitles.forEach(function(redirectTitle) {
apiDeleteRedir(redirectTitle);
});
// Check if talk pages were found
Line 4,405 ⟶ 5,132:
);
// Delete each talk page
redirectTalkPageIds.forEach(function(redirectTalkId) {
apiDeleteRedirTalk(redirectTalkId);
});
};
Line 4,417 ⟶ 5,140:
if ( !result.query || !result.query.pages ) {
// No results
self.addWarning('
self.setStatus('
return;
}
Line 4,434 ⟶ 5,157:
// Check if redirects were found
if ( redirectTitles.length === 0 ) {
self.addWarning('
self.setStatus('
return;
}
Line 4,441 ⟶ 5,164:
// Warn if there will be mass action, allow user to cancel
if ( redirectTitles.length >= 10 ) {
var processAction = function(action) {
if ( action === 'accept' ) {
doDeleteRedirects();
} else if ( action === 'show' ) {
title: '
message: '
'
actions: [
{ label:'
{ label:'
],
})
.then(processAction);
} else {
self.setStatus('skipped');
}
};
title: '
message: '
actions: [
{ label:'
{ label:'
{ label:'
],
size: 'medium'
})
.then(processAction);
} else {
doDeleteRedirects();
Line 4,489 ⟶ 5,215:
.done( processRedirects )
.fail( function(code, jqxhr) {
self.addApiError(code, jqxhr, '
self.setStatus('failed');
} );
Line 4,504 ⟶ 5,230:
var pageTitles = self.discussion.getPageTitles(self.pages);
var redirectTitles = [];
// Ignore the following titles, and any of their subpages
var ignoreTitleBases = [
'Template:WPUnited States Article alerts',
'Template:Article alerts columns',
'Template:
];
var getBase = function(title) {
return title.split('/')[0];
};
var blresults = [];
var iuresults = [];
//convert results (arrays of objects) to titles (arrays of strings), removing duplicates
var flattenToTitles = function(
return
function(
if (
if (
redirectTitles.push(
}
return
function(
if (
) {
return
} else {
return
}
},
Line 4,537 ⟶ 5,267:
);
} else if (
ignoreTitleBases.includes(getBase(result.title))
) {
return
} else {
return
}
},
Line 4,551 ⟶ 5,281:
};
/**
* @param {String} pageTitle
* @param {String} wikitext
* @returns {Promise(String)} updated wikitext, with any list items either removed or unlinked
*/
var checkListItems = function(pageTitle, wikitext, isMajorEdit) {
// Find lines marked with {{subst:void}}, and the preceding section heading (if any)
var toReview = /^{{subst:void}}(.*)$/m.exec(wikitext);
if ( !toReview ) {
// None found,
return $.Deferred().resolve(wikitext, !!isMajorEdit).promise();
}
// Find the preceding heading, if any
var precendingText = wikitext.split('{{subst:void}}')[0];
var allHeadings = precendingText.match(/^=+.+?=+$/gm);
var heading = ( !allHeadings ) ? null : allHeadings[allHeadings.length - 1]
.replace(/(^=* *| *=*$)/g, '')
.replace(/\[\[([^\|\]]*?)\|([^\]]*?)\]\]/, '$2')
.replace(/\[\[([^\|\]]*?)\]\]/, '$1');
// Prompt user
return multiButtonConfirm({
title: 'Review unlinked list item',
message: '<p>A backlink has been removed from the following list item:</p>' +
'<strong>List:</strong> [[' + pageTitle +
( heading
? '#' + mw.util.wikiUrlencode(heading) + '|' + pageTitle + '#' + heading + ']]'
: ']]'
) +
'<pre>' + toReview[1] + '</pre>' +
'<p>Please check if the item matches the list\'s [[WP:LISTCRITERIA|selection criteria]]'+
' before deciding to keep or remove the item from the list.</p>',
actions: [
{ label:'Keep item', action:'keep', icon:'articleCheck', flags:'progressive' },
{ label:'Keep and request citation', action:'keep-cite', icon:'flag' },
{ label:'Remove item', action:'remove', icon:'trash', flags:'destructive'}
],
size: 'large'
})
.then(function(action) {
if ( action === 'keep' ) {
// Remove the void from the start of the line
wikitext = wikitext.replace(/^{{subst:void}}/m, '');
} else if ( action === 'keep-cite' ) {
// Remove the void from the start of the line, add citation needed at the end
wikitext = wikitext.replace(/^{{subst:void}}(.*)(\n?)/m, '$1{{subst:Citation needed}}$2');
} else {
// Remove the whole line, mark as a major edit
wikitext = wikitext.replace(/^{{subst:void}}.*\n?/m, '');
isMajorEdit = true;
}
// Iterate, in case there is more to be reviewed
return checkListItems(pageTitle, wikitext, isMajorEdit);
});
};
// Function to transform a simplified API page object into edit parameters for API.editWithRetry
var confirmationPromises = [$.Deferred()]; // Each transformation must wait for the previous one to finish dipslaying confirmation prompts
var transform = function(page) {
var previousPromise = confirmationPromises[confirmationPromises.length-1];
var thisConfirmationPromise = $.Deferred();
confirmationPromises.push(thisConfirmationPromise);
return previousPromise.then(function(){
var oldWikitext = page.content;
var newWikitext = extraJs.unlink(
oldWikitext,
Line 4,633 ⟶ 5,351:
!!page.categories
);
if (
thisConfirmationPromise.resolve();
return $.Deferred().reject('skippedNoLinks');
}
var checkListItemsPromise = checkListItems(page.title, newWikitext);
checkListItemsPromise.then(thisConfirmationPromise.resolve);
return checkListItemsPromise.then(function(updatedWikitext, isMajorEdit) {
var req = {
text: newWikitext,
summary: 'Removing link(s)' +
( isMajorEdit ? ' / list item(s)' : '') +
(( config.xfd.type === 'ffd' ) ? ' / file usage(s)' : '' ) +
': [[' + self.discussion.getNomPageLink() + ']] closed as ' +
self.inputData.getResult() + config.script.advert,
nocreate: 1
};
if ( !isMajorEdit ) {
req.minor = 1;
}
return req;
});
});
};
Line 4,664 ⟶ 5,388:
// Check if, after flattening, there are still backlinks or image uses
if ( blresults.length === 0 && iuresults.length === 0 ) {
self.addWarning('
self.setStatus('skipped');
return;
}
// Ask user for confirmation
Line 4,684 ⟶ 5,409:
'<p>Use with caution, after reviewing the pages listed below. '+
'Note that the use of high speed, high volume editing software (such as this tool and '+
'Twinkle\'s unlink tool) is subject to the Bot policy\'s [[WP:ASSISTED|Assisted editing guidelines]] '+
'
var list = '<ul>';
if ( blresults.length !== 0 ) {
Line 4,695 ⟶ 5,420:
list += '<ul>';
title: heading,
message: para + list,
actions: [
{ label: '
{ label: '
],
size: 'medium'
})
.then(function(action) {
if ( action ) {
var unlinkTitles = iuresults.concat(blresults);
self.showTrackingProgress = 'unlink';
// Define api callbacks outside of loop
var onSuccess = function() { self.track('unlink', true); };
var onFailure = function(code, error, title) {
switch(code) {
self.track('unlink', false);
break;
default:
self.addApiError(code, error, [
'Could not remove backlinks from ',
extraJs.makeLink(title)
]);
self.track('unlink', false);
}
}
var onReadFail = function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
self.addApiError(code, jqxhr, 'Could not read contents of pages; could not remove backlinks');
self.setStatus('failed');
} // other errors handled above
};
// loop over unlinkTitles in lots of 50 (max for Api)
for (var ii=0; ii<unlinkTitles.length; ii+=50) {
API.editWithRetry(
unlinkTitles.slice(ii, ii+49).join('|'),
{
prop: 'categories|revisions',
clcategories: 'Category:All disambiguation pages',
},
transform,
onSuccess,
onFailure
).fail( onReadFail );
}
confirmationPromises[0].resolve();
} else {
self.addWarning('Cancelled by user');
self.setStatus('skipped');
}
});
};
Line 4,760 ⟶ 5,512:
.done( processBacklinks )
.fail( function(code, jqxhr) {
self.addApiError(code, jqxhr, '
self.setStatus('failed');
// Allow delete redirects task to begin
Line 4,810 ⟶ 5,562:
var pageTitles = self.discussion.getPageTitles(self.pages);
var deleteFirst = self.inputData.deleteFirst;
var softRedirect = self.inputData.result === '
var rcats = self.inputData.rcats;
var rcatshell = ( rcats ) ? "\n\n{{Rcat shell|\n" + rcats + "\n}}" : '';
self.setupTracking('redir', pageTitles.length);
// Make a redirect
var apiMakeRedirect = function(pageTitle, isRetry) {
var newWikitext;
if (
var targetPage = self.inputData.getTargetLink(pageTitle, true);
if ( targetPage.indexOf('Module:') !== 0 ) {
self.track('redir', false);
self.addError([
'Could not redirect ',
extraJs.makeLink(pageTitle),
' because target (',
extraJs.makeLink(targetPage),
') is not a module'
], true);
return false;
}
newWikitext = 'return require( "' + targetPage + '" )';
} else if ( softRedirect ) {
newWikitext = '{{Soft redirect|' + self.inputData.getTargetLink(pageTitle, true) +
'}}' + rcatshell;
Line 4,835 ⟶ 5,596:
title: pageTitle,
text: newWikitext,
summary: '[[:' + self.discussion.getNomPageLink() + ']]
self.inputData.getResult() + config.script.advert
} )
Line 4,842 ⟶ 5,603:
} )
.fail( function(code, jqxhr) {
if (!isRetry) {
apiMakeRedirect(pageTitle, true);
}
self.track('redir', false);
self.addApiError(code, jqxhr, [
'Could not edit page ',
extraJs.makeLink(pageTitle)
]);
Line 4,851 ⟶ 5,615:
// Delete before redirecting
var apiDelAndRedir = function(pageTitle, isRetry) {
API.postWithToken( 'csrf', {
action: 'delete',
Line 4,861 ⟶ 5,625:
} )
.fail( function(code, jqxhr) {
if (!isRetry) {
apiDelAndRedir(pageTitle, true);
}
self.track('redir', false);
self.addApiError(code, jqxhr, [
'Could not delete page ',
extraJs.makeLink(pageTitle)
]);
Line 4,891 ⟶ 5,658:
self.setupTracking('uncircle', targetTitles.length);
// Function to transform a simplified API page object into edit parameters for API.editWithRetry
var
var oldWikitext = page.content;
// Don't remove selflinks
var unlinkPageTitles = pageTitles.filter(function(t){
return t !== page.title;
});
var newWikitext = extraJs.unlink(oldWikitext, unlinkPageTitles);
// Check if any changes need to be made; if redirect target is a nominated page, remove
// nomination template and adjust edit summary
if ( newWikitext === oldWikitext ) {
// No links to unlink
return $.Deferred().reject("noCircularRedirects");
}
if ( $.inArray(page.title, pageTitles) !== -1 ) {
// Target is one of the nominated pages - also try to remove nom template if present
try {
newWikitext = config.xfd.removeNomTemplate(newWikitext);
} catch(e){
// Warning if multiple nom templates found
return $.Deferred().reject("multipleNomTemplates", e);
}
}
return {
text: newWikitext,
summary: '
']]
}
};
API.editWithRetry(
targetTitles,
null,
transform,
function() { self.track('edit', true); },
function(code, error, title) {
switch(code) {
case "noCircularRedirects":
self.addWarning('none found');
self.track('uncircle', false);
break;
case "multipleNomTemplates":
self.addError([
error.message + ' ',
extraJs.makeLink(title)
]);
self.track('uncircle', false); break;
default:
self.addApiError(code, error, [
'Could not edit page ',
extraJs.makeLink(title)
]);
self.track('uncircle', false);
}
}
).fail( function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
self.addApiError(code, jqxhr,
'Could not read contents of redirect target' + ( targetTitles.length > 1 ) ? 's' : '');
self.setStatus('failed');
} // other errors handled above
} );
};
Line 4,964 ⟶ 5,733:
var now = new Date();
var today = now.getUTCFullYear() + ' ' + config
' ' + now.getUTCDate();
var todaysLogpage = config.xfd.path + today;
Line 4,971 ⟶ 5,740:
if ( config.xfd.type !== 'mfd' ) {
self.discussion.taskManager.getTaskByName('updateNewLogPage').setDescription([
'Adding to ',
extraJs.makeLink(todaysLogpage, "
]);
}
var
action: 'query',
titles: self.discussion.nomPage,
prop: 'revisions',
indexpageids: 1,
rawcontinue: 1,
rvprop: 'content|timestamp',
rvslots: 'main',
rvsection: self.discussion.sectionNumber,
curtimestamp: 1
};
if ( config.xfd.type === 'afd' ) {
$.extend(query, {
list: 'embeddedin',
eititle: self.discussion.nomPage,
einamespace: config.xfd.ns_logpages,
eifilterredir: 'nonredirects',
eilimit: 500
});
// Need to fetch whole page, in order to check if afd is already closed
delete query.rvsection;
}
API.get( query )
.then( function (result) {
// Discussion wikitext
var id = result.query.pageids;
var oldWikitext = result.query.pages[ id ].revisions[0].slots.main['*'];
var heading = oldWikitext.slice(0, oldWikitext.indexOf('\n'));
// Abort if discussion is already closed
if ( oldWikitext.indexOf('xfd-closed') !== -1 ) {
self.discussion.taskManager.abortTasks('
return;
}
Line 5,063 ⟶ 5,783:
// Relist template
var relists = oldWikitext
.match(/\[\[Wikipedia:Deletion process#Relisting discussions\|Relisted\]\]/g);
var relistTemplate = '\n{{subst:Relist|1=' + self.inputData.getRelistComment() +
'|2=' + (( relists ) ? relists.length + 1 : 1) + '}}\n';
Line 5,076 ⟶ 5,796:
// Update link to log page
newWikitext = newWikitext.replace(
/\[\[
'[[' + todaysLogpage + '#'
);
Line 5,083 ⟶ 5,803:
// Discussion on old log page gets closed
var xfdCloseTop = config.xfd.wikitext.closeTop
.replace(/__RESULT__/, '
.replace(/__TO_TARGET__/, ' on [[' + todaysLogpage + '#' +
self.discussion.sectionHeader + '|' + today + ']]
.replace(/__RATIONALE__/, '.')
.replace(/__SIG__/, config.user.sig);
Line 5,092 ⟶ 5,812:
if ( !self.discussion.isBasicMode() ) {
pagesList = self.discussion.pages.reduce(function(list, page) {
var namespaceParam = ( page.getNamespaceId() === 828 ) ? '|module=Module' : '';
return list + config.xfd.wikitext.pagelinks.replace('__PAGE__', page.getMain() + namespaceParam);
}, '');
}
Line 5,122 ⟶ 5,843:
oldLogWikitext = topWikitext + '\n{{subst:rfd relisted|page=' + today +
'|' + self.discussion.sectionHeader + '}}';
} else if ( config.xfd.type === 'cfd' ) {
oldLogWikitext = '====' + self.discussion.sectionHeader + '====' +
'\n{{subst:cfd relisted|' + self.discussion.sectionHeader + '}}';
}
Line 5,128 ⟶ 5,852:
'today': today,
'newWikitext': newWikitext,
'oldLogWikitext': oldLogWikitext,
'nomPageTimestamps': {
start: result.curtimestamp,
base: result.query.pages[ id ].revisions[0].timestamp
}
};
Line 5,139 ⟶ 5,867:
// Abort if none found
if ( eiLogpages.length === 0 ) {
self.addError('
self.discussion.taskManager.abortTasks('');
return;
Line 5,147 ⟶ 5,875:
for (var i = 1; i < eiLogpages.length; i++) {
self.addWarning([
'Note: transcluded on additional log page: ',
extraJs.makeLink(
eiLogpages[i].title,
Line 5,159 ⟶ 5,887:
// Abort if old log page is actually today's logpage
if ( oldLogpage.title === todaysLogpage ) {
self.addError('Already transcluded to today\'s log page');
self.discussion.taskManager.abortTasks('');
return;
Line 5,166 ⟶ 5,894:
self.discussion.taskManager.getTaskByName('updateOldLogPage').setDescription([
'Removing from ',
extraJs.makeLink(oldLogpage.title, '
]);
// Store old log title
Line 5,176 ⟶ 5,904:
titles: oldLogpage.title + '|' + todaysLogpage,
prop: 'revisions',
rvprop: 'content|timestamp',
rvslots: 'main',
indexpageids: 1,
rawcontinue: '',
"curtimestamp": 1
} )
.
// Get wikitext
var newlogtext = '';
var oldlogtext = '';
var ids = result.query.pageids;
// Check how many log pages were found
if ( ids.length === 1 ) {
// Abort if only one log page found
self.discussion.taskManager.abortTasks(
'discussion already transcluded to today\'s log page'
);
return;
}
var newLogBaseTimestamp, oldLogBaseTimestamp;
// Identify log pages
if ( result.query.pages[ ids[0] ].title === todaysLogpage ) {
// ids[0] is the new log page
newlogtext = result.query.pages[ ids[0] ].revisions[0].slots.main['*'];
newLogBaseTimestamp = result.query.pages[ ids[0] ].revisions[0].timestamp;
oldlogtext = result.query.pages[ ids[1] ].revisions[0].slots.main['*'];
oldLogBaseTimestamp = result.query.pages[ ids[1] ].revisions[0].timestamp;
} else {
// ids[0] is the old log page
newlogtext = result.query.pages[ ids[1] ].revisions[0].slots.main['*'];
newLogBaseTimestamp = result.query.pages[ ids[1] ].revisions[0].timestamp;
oldlogtext = result.query.pages[ ids[0] ].revisions[0].slots.main['*'];
oldLogBaseTimestamp = result.query.pages[ ids[0] ].revisions[0].timestamp;
}
// Abort if already relisted
var t = mw.util.escapeRegExp(self.discussion.nomPage);
var oldPatt = new RegExp('<!-- ?\\{\\{' + t + '\\}\\} ?-->', 'i');
var newPatt = new RegExp('\\{\\{' + t + '\\}\\}', 'i');
if ( oldPatt.test(oldlogtext) || newPatt.test(newlogtext) ) {
self.discussion.taskManager.abortTasks('discussion has been relisted already');
return;
}
// Updated new log wikitext:
var newlogreg = new RegExp('<!-- Add new entries to the TOP of the following list -->','i');
self.discussion.taskManager.relistInfo.newLogWikitext = newlogtext.replace(
newlogreg,
'<!-- Add new entries to the TOP of the following list -->\n{{' +
self.discussion.nomPage + '}}<!--Relisted-->'
);
self.discussion.taskManager.relistInfo.newLogTimestamps = {
start: result.curtimestamp,
base: newLogBaseTimestamp
};
// Updated old log wikitext:
var oldlogreg = new RegExp("(\\{\\{" + t + "\\}\\})", 'i' );
self.discussion.taskManager.relistInfo.oldlogTransclusion = oldlogreg.test(oldlogtext);
self.discussion.taskManager.relistInfo.oldLogWikitext = oldlogtext.replace(
oldlogreg, '<!-- $1 -->');
self.discussion.taskManager.relistInfo.oldLogTimestamps = {
start: result.curtimestamp,
base: oldLogBaseTimestamp
};
// Ready to proceed to next tasks
self.track('done', true);
}, function(code, jqxhr) {
self.addApiError(code, jqxhr, 'Could not read contents of log pages');
self.discussion.taskManager.abortTasks('');
} );
} else if ( config.xfd.type === 'tfd' || config.xfd.type === 'rfd' || config.xfd.type === 'cfd' ) {
//New discussions on top of log page, so need to check out current log page wikitext
API.get( {
Line 5,192 ⟶ 5,984:
titles: todaysLogpage,
prop: 'revisions',
rvprop: 'content|timestamp',
rvslots: 'main',
indexpageids: 1,
rawcontinue: ''
} )
.then(function(response) {
var page = pageFromResponse(response);
var logWikitext = page.revisions[ 0 ].slots.main['*'];
var h4_match = /====\s*(.*?)\s*====/.exec(logWikitext);
var h4 = h4_match && h4_match[1];
if ( h4 ) {
// there is at least 1 level 4 heading on page - can edit section #2
self.discussion.taskManager.relistInfo.newLogSection = 2;
if ( h4.toUpperCase() === "NEW NOMINATIONS" ) {
self.discussion.taskManager.relistInfo.newLogEditType = 'appendtext';
} else {
self.discussion.taskManager.relistInfo.newLogEditType = 'prependtext';
}
} else {
// there are no level 4 headings on page - can append to section #1
self.discussion.taskManager.relistInfo.newLogEditType = 'appendtext';
self.discussion.taskManager.relistInfo.newLogSection = 1;
}
self.discussion.taskManager.relistInfo.newLogTimestamps = {
start: response.curtimestamp,
base: page.revisions[ 0 ].timestamp
};
// Ready to proceed to next tasks
self.track('done', true);
}, function(code, jqxhr) {
self.addApiError(code, jqxhr, [
'Could not read contents of ',
extraJs.makeLink(todaysLogpage, "today's log page")
]);
self.discussion.taskManager.abortTasks('');
Line 5,209 ⟶ 6,027:
self.track('done', true);
}
}, function(code, jqxhr) {
self.addApiError(code, jqxhr,
[ '
extraJs.makeLink(self.discussion.nomPage),
'; could not relist discussion' ]
);
self.discussion.taskManager.abortTasks('');
Line 5,257 ⟶ 6,051:
title: self.discussion.nomPage,
text: relistInfo.newWikitext,
summary: '
// Protect against errors and conflicts
assert: 'user',
basetimestamp: relistInfo.nomPageTimestamps.base,
starttimestamp: relistInfo.nomPageTimestamps.start
};
if ( config.xfd.type === 'mfd' ) {
Line 5,269 ⟶ 6,067:
.fail( function(code, jqxhr) {
self.track('edit', false);
self.addApiError(code, jqxhr, '
' discussion');
} );
};
Line 5,287 ⟶ 6,085:
title: ( config.xfd.type === 'afd' ) ? relistInfo.oldlogtitle : self.discussion.nomPage,
text: relistInfo.oldLogWikitext,
summary: (( config.xfd.type === 'afd' ) ? 'Relisting [[:' + self.discussion.nomPage +
']]
config.xfd.path + relistInfo.today + '#' + self.discussion.sectionHeader +
'|' + relistInfo.today + ']]
};
if (relistInfo.oldLogTimestamps) {
params.basetimestamp = relistInfo.oldLogTimestamps.base;
params.starttimestamp = relistInfo.oldLogTimestamps.start;
}
if ( config.xfd.type === 'afd' ) {
Line 5,297 ⟶ 6,100:
if ( !relistInfo.oldlogTransclusion ) {
self.track('edit', false);
self.addError('Transclusion not found on old log page; could not be commented out');
return;
}
Line 5,310 ⟶ 6,113:
.fail( function(code, jqxhr) {
self.track('edit', false);
self.addApiError(code, jqxhr, '
' log page');
} );
};
Line 5,330 ⟶ 6,133:
action: 'edit',
title: config.xfd.path + relistInfo.today,
summary: '
']]' : '"' + self.discussion.sectionHeader + '"') + config.script.advert
};
Line 5,338 ⟶ 6,141:
params[relistInfo.newLogEditType] = relistInfo.newWikitext;
}
if (relistInfo.newLogTimestamps) {
params.basetimestamp = relistInfo.newLogTimestamps.base;
params.starttimestamp = relistInfo.newLogTimestamps.start;
}
if ( /(tfd|rfd|cfd)/.test(config.xfd.type) ) {
params.section = relistInfo.newLogSection;
}
Line 5,349 ⟶ 6,156:
.fail( function(code, jqxhr) {
self.track('edit', false);
self.addApiError(code, jqxhr, '
' log page');
} );
};
Line 5,360 ⟶ 6,167:
var relistInfo = self.discussion.taskManager.relistInfo;
//var todayParts = relistInfo.today.split(' ');
var pageTitles = self.discussion.getPageTitles(null, {'moduledocs':true});
self.setupTracking('edit', pageTitles.length);
// Function to transform a simplified API page object into edit parameters for API.editWithRetry
var transform = function(page) {
// Check there's a corresponding nominated page
var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true});
if ( !pageObj ) {
return $.Deferred().reject("unexpectedTitle");
}
// Check corresponding page exists
if ( page.missing ) {
return $.Deferred().reject("doesNotExist");
}
var originalWikitext = page.content;
var updatedWikitext;
try {
updatedWikitext = config.xfd.updateNomTemplateAfterRelist(
originalWikitext,
relistInfo.today,
self.discussion.sectionHeader
);
} catch(e){
return $.Deferred().reject("couldNotUpdate", e);
}
var noChangesToMake = updatedWikitext === originalWikitext;
if ( noChangesToMake ) {
return $.Deferred().reject("nominationTemplateNotFound");
}
return {
text: updatedWikitext,
summary: '
'
}
};
API.editWithRetry(
pageTitles,
null,
transform,
function() { self.track('edit', true); },
function(code, error, title) {
switch(code) {
case "unexpectedTitle":
self.addError([
'API query result included unexpected title ',
extraJs.makeLink(
'; this talk page will not be edited'
]);
self.track('edit', false);
case "doesNotExist":
self.addError([
extraJs.makeLink(title),
' does not exist, and will not be edited'
]);
self.track('processed', false);
break;
case "couldNotUpdate":
self.addError([
'Could not update ',
extraJs.makeLink(title),
': ',
error.message
]);
self.track('edit', false);
break;
case "nominationTemplateNotFound":
self.addWarning([
'Nomination template not found on page ',
extraJs.makeLink(title)
]);
self.track('edit', false);
break;
default:
self.addApiError(code, error, [
'Could not edit page ',
extraJs.makeLink(title)
]);
self.track('edit', false);
}
}
).fail( function(errortype, code, jqxhr) {
if ( errortype === "read" ) {
( pageTitles.length > 1 ) ? 's' : '');
self.setStatus('failed');
} // other errors handled above
} );
};
Line 5,454 ⟶ 6,271:
// Determine previous state from localStorage
try {
if ( !!window.localStorage.getItem('xfdc-closedHidden') ) {
this.isHidden = true;
} else {
Line 5,469 ⟶ 6,286:
this.isHidden = true;
try {
window.localStorage.setItem('xfdc-closedHidden', true);
} catch(e) {}
$('.xfd-closed, .tfd-closed, #XFDcloser-showhide-hide').hide();
Line 5,478 ⟶ 6,295:
this.isHidden = false;
try {
window.localStorage.setItem('xfdc-closedHidden', '');
} catch(e) {}
$('.xfd-closed, .tfd-closed, #XFDcloser-showhide-hide').show();
Line 5,491 ⟶ 6,308:
$('<a>')
.attr('id', 'XFDcloser-showhide-hide')
.text('Hide closed discussions')
.toggle(!self.isHidden)
.on('click', self.hideClosed),
$('<a>')
.attr('id', 'XFDcloser-showhide-show')
.text('
.toggle(self.isHidden)
.on('click', self.showClosed)
Line 5,517 ⟶ 6,334:
if ( d ) {
try {
if ( d.
d.showLinks();
} else {
d.retrieveExtraInfo()
.then(
function() { d.showLinks(); },
function(failMessage) {
// set "basic" mode, show "basic" links with the failure message
d.pages = false;
d.showLinks(failMessage);
}
);
}
} catch(e) {
console.warn('[XFDcloser] Could not retrieve page info for ' + $(this).text() +
' [see error below]');
console.warn(e);
}
Line 5,532 ⟶ 6,361:
/* ========== End of full file closure wrappers ================================================ */
});
/* </nowiki> */
|