@@ -37,6 +37,7 @@ import errno | |||||
from getpass import getpass | from getpass import getpass | ||||
from os import chmod, path | from os import chmod, path | ||||
import stat | import stat | ||||
import re | |||||
from sys import argv | from sys import argv | ||||
import time | import time | ||||
from urllib import urlencode | from urllib import urlencode | ||||
@@ -45,8 +46,10 @@ import earwigbot | |||||
import git | import git | ||||
SCRIPT_SITE = "en.wikipedia.org" | SCRIPT_SITE = "en.wikipedia.org" | ||||
SCRIPT_TEST = "test.wikipedia.org" | |||||
SCRIPT_USER = "The Earwig" | SCRIPT_USER = "The Earwig" | ||||
SCRIPT_FILE = "tfdclerk.js" | SCRIPT_FILE = "tfdclerk.js" | ||||
SCRIPT_SDIR = "src" | |||||
COOKIE_FILE = ".cookies" | COOKIE_FILE = ".cookies" | ||||
REPLACE_TAG = "@TFDCLERK_{tag}@" | REPLACE_TAG = "@TFDCLERK_{tag}@" | ||||
EDIT_SUMMARY = "Updating script with latest version ({version})" | EDIT_SUMMARY = "Updating script with latest version ({version})" | ||||
@@ -55,9 +58,6 @@ SCRIPT_PAGE = "User:{user}/{file}".format(user=SCRIPT_USER, file=SCRIPT_FILE) | |||||
SCRIPT_ROOT = path.dirname(path.abspath(__file__)) | SCRIPT_ROOT = path.dirname(path.abspath(__file__)) | ||||
REPO = git.Repo(SCRIPT_ROOT) | REPO = git.Repo(SCRIPT_ROOT) | ||||
if len(argv) > 1 and argv[1].lstrip("-").startswith("t"): | |||||
SCRIPT_SITE = "test.wikipedia.org" | |||||
def _is_clean(): | def _is_clean(): | ||||
""" | """ | ||||
Return whether there are uncommitted changes in the working directory. | Return whether there are uncommitted changes in the working directory. | ||||
@@ -78,6 +78,28 @@ def _get_full_version(): | |||||
datefmt = time.strftime("%H:%M, %-d %B %Y (UTC)", date) | datefmt = time.strftime("%H:%M, %-d %B %Y (UTC)", date) | ||||
return "{hash} ({date})".format(hash=_get_version(), date=datefmt) | return "{hash} ({date})".format(hash=_get_version(), date=datefmt) | ||||
def _do_include(text, include): | |||||
""" | |||||
Replace an include directive inside the script with a source file. | |||||
""" | |||||
with open(path.join(SCRIPT_ROOT, SCRIPT_SDIR, include), "r") as fp: | |||||
source = fp.read().decode("utf8") | |||||
hs_tag = REPLACE_TAG.format(tag="HEADER_START") | |||||
he_tag = REPLACE_TAG.format(tag="HEADER_END") | |||||
if hs_tag in source and he_tag in source: | |||||
lines = source.splitlines() | |||||
head_start = [i for i, line in enumerate(lines) if hs_tag in line][0] | |||||
head_end = [i for i, line in enumerate(lines) if he_tag in line][0] | |||||
del lines[head_start:head_end + 1] | |||||
source = "\n".join(lines) | |||||
tag = REPLACE_TAG.format(tag="INCLUDE:" + include) | |||||
if text[:text.index(tag)][-2:] == "\n\n" and source.startswith("\n"): | |||||
source = source[1:] # Remove extra newline | |||||
return text.replace(tag, source) | |||||
def _get_script(): | def _get_script(): | ||||
""" | """ | ||||
Return the complete script. | Return the complete script. | ||||
@@ -85,6 +107,11 @@ def _get_script(): | |||||
with open(path.join(SCRIPT_ROOT, SCRIPT_FILE), "r") as fp: | with open(path.join(SCRIPT_ROOT, SCRIPT_FILE), "r") as fp: | ||||
text = fp.read().decode("utf8") | text = fp.read().decode("utf8") | ||||
re_include = REPLACE_TAG.format(tag=r"INCLUDE:(.*?)") | |||||
includes = re.findall(re_include, text) | |||||
for include in includes: | |||||
text = _do_include(text, include) | |||||
replacements = { | replacements = { | ||||
"VERSION": _get_version(), | "VERSION": _get_version(), | ||||
"VERSION_FULL": _get_full_version() | "VERSION_FULL": _get_full_version() | ||||
@@ -111,7 +138,7 @@ def _get_cookiejar(): | |||||
return cookiejar | return cookiejar | ||||
def _get_site(): | |||||
def _get_site(site_url=SCRIPT_SITE): | |||||
""" | """ | ||||
Return the EarwigBot Site object where the script will be saved. | Return the EarwigBot Site object where the script will be saved. | ||||
@@ -119,7 +146,7 @@ def _get_site(): | |||||
user's password in a config file like EarwigBot normally does. | user's password in a config file like EarwigBot normally does. | ||||
""" | """ | ||||
site = earwigbot.wiki.Site( | site = earwigbot.wiki.Site( | ||||
base_url="https://" + SCRIPT_SITE, script_path="/w", | |||||
base_url="https://" + site_url, script_path="/w", | |||||
cookiejar=_get_cookiejar(), assert_edit="user") | cookiejar=_get_cookiejar(), assert_edit="user") | ||||
logged_in_as = site._get_username_from_cookies() | logged_in_as = site._get_username_from_cookies() | ||||
@@ -133,14 +160,23 @@ def main(): | |||||
""" | """ | ||||
Main entry point for script. | Main entry point for script. | ||||
""" | """ | ||||
if len(argv) > 1 and argv[1].lstrip("-").startswith("c"): | |||||
print(_get_script(), end="") | |||||
return | |||||
if not _is_clean(): | if not _is_clean(): | ||||
print("Uncommitted changes in working directory. Stopping.") | print("Uncommitted changes in working directory. Stopping.") | ||||
exit(1) | exit(1) | ||||
if len(argv) > 1 and argv[1].lstrip("-").startswith("t"): | |||||
site_url = SCRIPT_TEST | |||||
else: | |||||
site_url = SCRIPT_SITE | |||||
print("Uploading script to [[{page}]] on {site}...".format( | print("Uploading script to [[{page}]] on {site}...".format( | ||||
page=SCRIPT_PAGE, site=SCRIPT_SITE)) | |||||
page=SCRIPT_PAGE, site=site_url)) | |||||
script = _get_script() | script = _get_script() | ||||
site = _get_site() | |||||
site = _get_site(site_url) | |||||
page = site.get_page(SCRIPT_PAGE) | page = site.get_page(SCRIPT_PAGE) | ||||
summary = EDIT_SUMMARY.format(version=_get_version()) | summary = EDIT_SUMMARY.format(version=_get_version()) | ||||
@@ -0,0 +1,67 @@ | |||||
// @TFDCLERK_HEADER_START@ | |||||
/* | |||||
Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights to | |||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |||||
of the Software, and to permit persons to whom the Software is furnished to do | |||||
so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. | |||||
*/ | |||||
// @TFDCLERK_HEADER_END@ | |||||
TFDClerk = { | |||||
version: "@TFDCLERK_VERSION@", | |||||
script_url: "https://en.wikipedia.org/wiki/User:The_Earwig/tfdclerk.js", | |||||
author_url: "https://en.wikipedia.org/wiki/User_talk:The_Earwig", | |||||
github_url: "https://github.com/earwig/tfdclerk", | |||||
tfds: [], | |||||
_api: new mw.Api(), | |||||
_sysop: $.inArray("sysop", mw.config.get("wgUserGroups")) >= 0 | |||||
// TODO: access time? | |||||
}; | |||||
TFDClerk.api_get = function(tfd, params, done, fail, always) { | |||||
TFDClerk._api.get(params) | |||||
.done(function(data) { | |||||
if (done) | |||||
done.call(tfd, data); | |||||
}) | |||||
.fail(function(error) { | |||||
if (fail) | |||||
fail.call(tfd, error); | |||||
else | |||||
tfd._error("API query failure", error); | |||||
}) | |||||
.always(function() { | |||||
if (always) | |||||
always.call(tfd); | |||||
}); | |||||
}; | |||||
TFDClerk.install = function() { | |||||
$("h4").each(function(i, elem) { | |||||
var head = $(elem); | |||||
if (head.next().hasClass("tfd-closed")) | |||||
return; | |||||
if (head.find(".mw-editsection").length == 0) | |||||
return; | |||||
var tfd = new TFD(TFDClerk.tfds.length, head); | |||||
TFDClerk.tfds.push(tfd); | |||||
tfd.add_hooks(); | |||||
}); | |||||
}; |
@@ -0,0 +1,230 @@ | |||||
// @TFDCLERK_HEADER_START@ | |||||
/* | |||||
Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights to | |||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |||||
of the Software, and to permit persons to whom the Software is furnished to do | |||||
so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. | |||||
*/ | |||||
// @TFDCLERK_HEADER_END@ | |||||
TFD = function(id, head) { | |||||
this.id = id; | |||||
this.head = head; | |||||
this.box = null; | |||||
this._guard = false; | |||||
this._submit_blockers = []; | |||||
this._wikitext = undefined; | |||||
this._wikitext_callbacks = []; | |||||
}; | |||||
TFD.prototype._block_submit = function(reason) { | |||||
if (this._submit_blockers.indexOf(reason) < 0) | |||||
this._submit_blockers.push(reason); | |||||
this.box.find(".tfdclerk-submit").prop("disabled", true); | |||||
}; | |||||
TFD.prototype._unblock_submit = function(reason) { | |||||
var index = this._submit_blockers.indexOf(reason); | |||||
if (index >= 0) | |||||
this._submit_blockers.splice(index, 1); | |||||
if (this._submit_blockers.length == 0) | |||||
this.box.find(".tfdclerk-submit").prop("disabled", false); | |||||
}; | |||||
TFD.prototype._error = function(msg, extra) { | |||||
var elem = $("<span/>", { | |||||
html: "<strong>Error:</strong> " + (extra ? msg + ": " : msg), | |||||
style: "color: #A00;" | |||||
}); | |||||
if (extra) | |||||
elem.append($("<span/>", { | |||||
text: extra, | |||||
style: "font-family: monospace;" | |||||
})); | |||||
var contact = $("<a/>", { | |||||
href: TFDClerk.author_url, | |||||
title: TFDClerk.author_url.split("/").pop().replace(/_/g, " "), | |||||
text: "contact me" | |||||
}), file_bug = $("<a/>", { | |||||
href: TFDClerk.github_url, | |||||
title: TFDClerk.github_url.split("/").splice(-2).join("/"), | |||||
text: "file a bug report" | |||||
}); | |||||
elem.append($("<br/>")) | |||||
.append($("<small/>", { | |||||
html: "This may be caused by an edit conflict or other " + | |||||
"intermittent problem. Try refreshing the page. If the error " + | |||||
"persists, you can " + contact.prop("outerHTML") + " or " + | |||||
file_bug.prop("outerHTML") + "." | |||||
})); | |||||
elem.insertAfter(this.box.find("h5")); | |||||
this._block_submit("error"); | |||||
}; | |||||
TFD.prototype._get_discussion_page = function() { | |||||
var url = this.head.find(".mw-editsection a").first().prop("href"); | |||||
var match = url.match(/title=(.*?)(\&|$)/); | |||||
return match ? match[1] : null; | |||||
}; | |||||
TFD.prototype._get_section_number = function() { | |||||
var url = this.head.find(".mw-editsection a").first().prop("href"); | |||||
var match = url.match(/section=(.*?)(\&|$)/); | |||||
return match ? match[1] : null; | |||||
}; | |||||
TFD.prototype._with_content = function(callback) { | |||||
var title = this._get_discussion_page(), | |||||
section = this._get_section_number(); | |||||
if (title === null || section === null) | |||||
return this._error("couldn't find discussion section"); | |||||
if (this._wikitext !== undefined) { | |||||
if (this._wikitext === null) | |||||
this._wikitext_callbacks.push(callback); | |||||
else | |||||
callback.call(this, this._wikitext); | |||||
return; | |||||
} | |||||
this._wikitext = null; | |||||
TFDClerk.api_get(this, { | |||||
action: "query", | |||||
prop: "revisions", | |||||
rvprop: "content", | |||||
indexpageids: "", | |||||
rvsection: section, | |||||
titles: title | |||||
}, function(data) { | |||||
var pageid = data.query.pageids[0]; | |||||
var content = data.query.pages[pageid].revisions[0]["*"]; | |||||
this._wikitext = content; | |||||
callback.call(this, content); | |||||
for (var i in this._wikitext_callbacks) | |||||
this._wikitext_callbacks[i].call(this, content); | |||||
}, null, function() { | |||||
this._wikitext_callbacks = []; | |||||
}); | |||||
}; | |||||
TFD.prototype._remove_option_box = function() { | |||||
this.box.remove(); | |||||
this.box = null; | |||||
this._guard = false; | |||||
this._submit_blockers = []; | |||||
}; | |||||
TFD.prototype._add_option_box = function(verb, title, callback, options) { | |||||
var self = this; | |||||
this.box = $("<div/>", { | |||||
id: "tfdclerk-" + verb + "-box-" + this.id, | |||||
addClass: "tfdclerk-" + verb + "-box" | |||||
}) | |||||
.css("position", "relative") | |||||
.css("border", "1px solid #AAA") | |||||
.css("color", "#000") | |||||
.css("background-color", "#F9F9F9") | |||||
.css("margin", "0.5em 0") | |||||
.css("padding", "1em") | |||||
.append($("<div/>") | |||||
.css("position", "absolute") | |||||
.css("right", "1em") | |||||
.css("top", "0.5em") | |||||
.css("font-size", "75%") | |||||
.css("color", "#777") | |||||
.append($("<a/>", { | |||||
href: TFDClerk.script_url, | |||||
title: "tfdclerk.js", | |||||
text: "tfdclerk.js" | |||||
})) | |||||
.append($("<span/>", {text: " version " + TFDClerk.version}))) | |||||
.append($("<h5/>", { | |||||
text: title, | |||||
style: "margin: 0; padding: 0 0 0.25em 0;" | |||||
})); | |||||
options.call(this); | |||||
this.box.append($("<button/>", { | |||||
text: verb.charAt(0).toUpperCase() + verb.slice(1), | |||||
addClass: "tfdclerk-submit mw-ui-button mw-ui-progressive", | |||||
style: "margin-right: 0.5em;", | |||||
disabled: this._submit_blockers.length > 0, | |||||
click: function() { | |||||
self._block_submit("submitting"); | |||||
self.box.find(".tfdclerk-cancel").prop("disabled", true); | |||||
callback.call(self); | |||||
} | |||||
})) | |||||
.append($("<button/>", { | |||||
text: "Cancel", | |||||
addClass: "tfdclerk-cancel mw-ui-button", | |||||
click: function() { self._remove_option_box(); } | |||||
})) | |||||
.insertAfter(this.head); | |||||
}; | |||||
TFD.prototype._add_option_table = function(options) { | |||||
var table = $("<table/>", {style: "border-spacing: 0;"}); | |||||
$.each(options, function(i, opt) { | |||||
table.append($("<tr/>") | |||||
.append( | |||||
$("<td/>", { | |||||
style: "padding-bottom: 0.75em; padding-right: 0.5em;" | |||||
}).append(opt[0])) | |||||
.append( | |||||
$("<td/>", { | |||||
style: "padding-bottom: 0.75em;" | |||||
}).append(opt[1])) | |||||
); | |||||
}); | |||||
this.box.append(table); | |||||
}; | |||||
TFD.prototype._build_loading_node = function(node, text) { | |||||
return $("<" + node + "/>", { | |||||
text: text + "...", | |||||
style: "font-style: italic; color: #777;" | |||||
}); | |||||
}; | |||||
TFD.prototype._build_hook = function(verb, callback) { | |||||
var self = this; | |||||
return $("<span/>", {style: "margin-left: 1em;"}) | |||||
.append($("<span/>", {addClass: "mw-editsection-bracket", text: "["})) | |||||
.append($("<a/>", { | |||||
href: "#", | |||||
text: verb, | |||||
title: "tfdclerk: " + verb + " discussion", | |||||
click: function() { | |||||
callback.call(self); | |||||
return false; | |||||
} | |||||
})) | |||||
.append($("<span/>", {addClass: "mw-editsection-bracket", text: "]"})); | |||||
}; | |||||
TFD.prototype.add_hooks = function() { | |||||
$("<span/>", {addClass: "tfdclerk-hooks"}) | |||||
.append(this._build_hook("close", this.close)) | |||||
.append(this._build_hook("relist", this.relist)) | |||||
.appendTo(this.head.find(".mw-editsection")); | |||||
}; |
@@ -0,0 +1,293 @@ | |||||
// @TFDCLERK_HEADER_START@ | |||||
/* | |||||
Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights to | |||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |||||
of the Software, and to permit persons to whom the Software is furnished to do | |||||
so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. | |||||
*/ | |||||
// @TFDCLERK_HEADER_END@ | |||||
TFD.prototype._do_close = function() { | |||||
// TODO | |||||
// rough mockup: | |||||
// - post-submit ui updates | |||||
// - result options -> result string | |||||
// - disable comments box | |||||
// - collapse/disable/fix actions | |||||
// - add progress info area | |||||
// - "Closing discussion..." | |||||
// - add discussion close tags with result and comment, optional nac | |||||
// - " done ([[diff]])" | |||||
// - interface for closing each template | |||||
// - replace gray buttons with progressive refresh button | |||||
}; | |||||
TFD.prototype._is_merge = function() { | |||||
return this.head.nextUntil("h4").filter("p").first().find("b") | |||||
.text() == "Propose merging"; | |||||
}; | |||||
TFD.prototype._get_close_action_choices = function() { | |||||
// TODO | |||||
return [{ | |||||
id: "none", | |||||
name: "Do nothing", | |||||
help: "The script will not modify this template or pages related to " + | |||||
"it. You will need to carry out the closure yourself.", | |||||
on_select: null, | |||||
on_close: null, | |||||
default: true | |||||
}, { | |||||
id: "holding-cell", | |||||
name: "Move to holding cell", | |||||
help: "The script will add {{being deleted}} to the template and " + | |||||
"add an entry to the holding cell ([[WP:TFD/H]]).", | |||||
on_select: null, | |||||
on_close: null | |||||
}]; | |||||
}; | |||||
TFD.prototype._on_close_result_change = function() { | |||||
this._unblock_submit("no-close-result"); | |||||
// TODO: possibly disable/enable individual close actions | |||||
}; | |||||
TFD.prototype._build_close_results = function() { | |||||
if (this._is_merge()) | |||||
var choices = ["Merge", "Do not merge", "No consensus", "Other"]; | |||||
else | |||||
var choices = ["Delete", "Keep", "Redirect", "No consensus", "Other"]; | |||||
var self = this; | |||||
var elems = $("<div/>"); | |||||
$("<label/>").append($("<input/>", { | |||||
name: "result-speedy", | |||||
type: "checkbox", | |||||
value: "true" | |||||
})).append($("<span/>", { | |||||
text: "Speedy", | |||||
style: "margin: 0 1.25em 0 0.25em;" | |||||
})).appendTo(elems); | |||||
$.each(choices, function(i, choice) { | |||||
$("<label/>").append($("<input/>", { | |||||
name: "result", | |||||
type: "radio", | |||||
value: choice.toLowerCase(), | |||||
change: function() { self._on_close_result_change(); } | |||||
})).append($("<span/>", { | |||||
text: choice, | |||||
style: "margin: 0 1.25em 0 0.25em;" | |||||
})).appendTo(elems); | |||||
}); | |||||
var other = elems.children().last(); | |||||
other.find("span").text("Other:").css("margin", "0 0.25em"); | |||||
other.append($("<input/>", { | |||||
name: "result-other", | |||||
type: "text" | |||||
})); | |||||
this._block_submit("no-close-result"); | |||||
return elems; | |||||
}; | |||||
TFD.prototype._on_backlink_summary = function(page, tlinfo, ntrans, nmtrans, | |||||
nlinks) { | |||||
tlinfo.empty().append($("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl("Special:WhatLinksHere/" + page, { | |||||
namespace: "", | |||||
hidelinks: 1, | |||||
hideredirs: 1, | |||||
hidetrans: 0 | |||||
}), | |||||
title: "Transclusions of " + page, | |||||
text: ntrans + " transclusions" | |||||
}))); | |||||
if (ntrans != 0) | |||||
tlinfo.append($("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl("Special:WhatLinksHere/" + page, { | |||||
namespace: 0, | |||||
hidelinks: 1, | |||||
hideredirs: 1, | |||||
hidetrans: 0 | |||||
}), | |||||
title: "Mainspace transclusions of " + page, | |||||
text: nmtrans + " in mainspace" | |||||
}))); | |||||
if (nlinks != 0) | |||||
tlinfo.append($("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl("Special:WhatLinksHere/" + page, { | |||||
namespace: "", | |||||
hidelinks: 0, | |||||
hideredirs: 0, | |||||
hidetrans: 1 | |||||
}), | |||||
title: "Mainspace links to " + page, | |||||
text: nlinks + " mainspace links" | |||||
}))); | |||||
}; | |||||
TFD.prototype._load_backlink_summary = function(page, tlinfo) { | |||||
var limit = TFDClerk._sysop ? 5000 : 500; | |||||
TFDClerk.api_get(this, { | |||||
action: "query", | |||||
list: "embeddedin|backlinks", | |||||
eititle: page, | |||||
eilimit: limit, | |||||
bltitle: page, | |||||
blnamespace: "0|10", | |||||
bllimit: limit, | |||||
blredirect: "" | |||||
}, function(data) { | |||||
var ntrans = data.query.embeddedin.length; | |||||
var nmtrans = data.query.embeddedin.filter(function(pg) { | |||||
return pg.ns == 0; | |||||
}).length; | |||||
var nlinks = data.query.backlinks.reduce(function(acc, pg) { | |||||
var c = 0; | |||||
if (pg.ns == 0) | |||||
c++; | |||||
if (pg.redirlinks) | |||||
c += pg.redirlinks.filter(function(rl) { | |||||
return rl.ns == 0; | |||||
}).length; | |||||
return acc + c; | |||||
}, 0); | |||||
if (data["continue"]) { | |||||
if (data["continue"].eicontinue) { | |||||
ntrans += "+"; | |||||
nmtrans += "+"; | |||||
} | |||||
if (data["continue"].blcontinue) | |||||
nlinks += "+"; | |||||
} | |||||
this._on_backlink_summary(page, tlinfo, ntrans, nmtrans, nlinks); | |||||
}); | |||||
}; | |||||
TFD.prototype._build_close_action_entry = function(page) { | |||||
var self = this; | |||||
var redlink = this.head.nextUntil("h4").filter("ul").first() | |||||
.find("a").filter(function() { return $(this).text() == page; }) | |||||
.hasClass("new"); | |||||
var tlinfo = $("<ul/>", {style: "display: inline;"}) | |||||
.append(this._build_loading_node("li", "Fetching transclusions")); | |||||
this._load_backlink_summary(page, tlinfo); | |||||
var select_extra = $("<span/>"); | |||||
var help = $("<abbr/>", {text: "?"}); | |||||
var select = $("<select/>", { | |||||
disabled: redlink, | |||||
change: function() { | |||||
var option = select.find("option:selected"); | |||||
help.prop("title", option.data("help")); | |||||
if (option.data("select")) | |||||
option.data("select").call(self, select_extra); | |||||
} | |||||
}); | |||||
$.each(this._get_close_action_choices(), function(i, choice) { | |||||
select.append($("<option/>", { | |||||
value: choice.id, | |||||
text: choice.name, | |||||
selected: choice.default | |||||
}).data({ | |||||
help: choice.help, | |||||
select: choice.on_select, | |||||
close: choice.on_close | |||||
})); | |||||
if (choice.default) | |||||
help.prop("title", choice.help); | |||||
}); | |||||
return $("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl(page), | |||||
title: page, | |||||
text: page, | |||||
addClass: redlink ? "new" : "", | |||||
style: "font-weight: bold;" | |||||
})).append($("<span/>", {text: ": "})) | |||||
.append(select) | |||||
.append(select_extra) | |||||
.append($("<span/>", {text: " ("})) | |||||
.append(help) | |||||
.append($("<span/>", {text: ") ("})) | |||||
.append($("<div/>", {addClass: "hlist", style: "display: inline;"}) | |||||
.append(tlinfo)) | |||||
.append($("<span/>", {text: ")"})); | |||||
}; | |||||
TFD.prototype._add_close_actions = function() { | |||||
this._block_submit("add-close-actions"); | |||||
this._with_content(function(content) { | |||||
var regex = /\*\s*\{\{tfd links\|(.*?)(\||\}\})/gi, | |||||
match = regex.exec(content); | |||||
if (match === null) | |||||
return this._error("no templates found in section"); | |||||
var list = $("<ul/>", {style: "margin: 0 0 0 1em;"}); | |||||
do { | |||||
var page = "Template:" + match[1]; | |||||
this._build_close_action_entry(page).appendTo(list); | |||||
} while ((match = regex.exec(content)) !== null); | |||||
this.box.find(".tfdclerk-actions").empty().append(list); | |||||
this._unblock_submit("add-close-actions"); | |||||
}); | |||||
}; | |||||
TFD.prototype.close = function() { | |||||
if (this._guard) | |||||
return; | |||||
this._guard = true; | |||||
this._add_option_box("close", "Closing discussion", this._do_close, | |||||
function() { | |||||
this._add_option_table([ | |||||
[ | |||||
$("<span/>", {text: "Result:"}), | |||||
this._build_close_results() | |||||
], | |||||
[ | |||||
$("<label/>", { | |||||
for: "tfdclerk-comments-" + this.id, | |||||
text: "Comments:" | |||||
}), | |||||
$("<textarea/>", { | |||||
id: "tfdclerk-comments-" + this.id, | |||||
name: "comments", | |||||
rows: 2, | |||||
cols: 60, | |||||
placeholder: "Optional. Do not sign." | |||||
}) | |||||
], | |||||
[ | |||||
$("<span/>", {text: "Actions:"}), | |||||
$("<div/>", {addClass: "tfdclerk-actions"}) | |||||
.append(this._build_loading_node("span", "Fetching")), | |||||
] | |||||
]); | |||||
this._add_close_actions(); | |||||
}); | |||||
}; |
@@ -0,0 +1,132 @@ | |||||
// @TFDCLERK_HEADER_START@ | |||||
/* | |||||
Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights to | |||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |||||
of the Software, and to permit persons to whom the Software is furnished to do | |||||
so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. | |||||
*/ | |||||
// @TFDCLERK_HEADER_END@ | |||||
TFD.prototype._do_relist = function() { | |||||
// TODO | |||||
// rough mockup: | |||||
// - post-submit ui updates | |||||
// - date input box -> link to new discussion log page | |||||
// - disable comments box | |||||
// - add progress info area | |||||
// - "Adding discussion to new date..." | |||||
// - add discussion to new date, plus relist template | |||||
// - " done ([[diff]])" | |||||
// - "Removing discussion from old date..." | |||||
// - replace contents of section with {{relisted}} | |||||
// - " done ([[diff]])" | |||||
// - "Updating discussion link on template(s):\n* [[Template:A]]..." | |||||
// - update |link param of {{template for discussion}} | |||||
// - " done ([[diff]])" | |||||
// - replace gray buttons with progressive refresh button | |||||
}; | |||||
TFD.prototype._get_relist_date = function() { | |||||
return new Date(this.box.find(".tfdclerk-date").val()).toLogDateFormat(); | |||||
}; | |||||
TFD.prototype._on_date_change = function() { | |||||
var date = this._get_relist_date(); | |||||
var title = "Wikipedia:Templates for discussion/Log/" + date; | |||||
var info = this.box.find(".tfdclerk-discussion-info"); | |||||
info.empty(); | |||||
if (mw.config.get("wgTitle") == "Templates for discussion") | |||||
var this_date = this.head.prevAll().filter("h3").first().find("a") | |||||
.prop("title").split("/Log/")[1]; | |||||
else | |||||
var this_date = mw.config.get("wgTitle").split("/Log/")[1]; | |||||
if (date == null || date == this_date) { | |||||
this._block_submit("bad-date"); | |||||
info.append($("<span/>", { | |||||
text: date == this_date ? "Same as current date" : "Invalid date", | |||||
style: "color: #A00;" | |||||
})); | |||||
return; | |||||
} | |||||
this._unblock_submit("bad-date"); | |||||
this._block_submit("checking-discussion"); | |||||
info.append(this._build_loading_node("span", "Checking discussion")); | |||||
TFDClerk.api_get(this, { | |||||
action: "query", | |||||
indexpageids: "", | |||||
titles: title | |||||
}, function(data) { | |||||
this._unblock_submit("checking-discussion"); | |||||
info.empty().append($("<span/>", {text: "Log: "})).append($("<a/>", { | |||||
href: mw.util.getUrl(title), | |||||
title: title, | |||||
text: date, | |||||
addClass: data.query.pageids[0] < 0 ? "new" : "" | |||||
})); | |||||
if (date == (new Date().toLogDateFormat())) | |||||
info.append($("<span/>", {text: " (today)"})); | |||||
}); | |||||
}; | |||||
TFD.prototype.relist = function() { | |||||
var self = this; | |||||
if (this._guard) | |||||
return; | |||||
this._guard = true; | |||||
this._add_option_box("relist", "Relisting discussion", this._do_relist, | |||||
function() { | |||||
this._add_option_table([ | |||||
[ | |||||
$("<label/>", { | |||||
for: "tfdclerk-date-" + this.id, | |||||
text: "New date:" | |||||
}), | |||||
$("<input/>", { | |||||
id: "tfdclerk-date-" + this.id, | |||||
addClass: "tfdclerk-date", | |||||
name: "date", | |||||
type: "date", | |||||
value: new Date().toDatePickerFormat(), | |||||
change: function() { self._on_date_change(); } | |||||
}).add($("<span/>", { | |||||
addClass: "tfdclerk-discussion-info", | |||||
style: "margin-left: 0.5em; vertical-align: middle;" | |||||
})) | |||||
], | |||||
[ | |||||
$("<label/>", { | |||||
for: "tfdclerk-comments-" + this.id, | |||||
text: "Comments:" | |||||
}), | |||||
$("<textarea/>", { | |||||
id: "tfdclerk-comments-" + this.id, | |||||
name: "comments", | |||||
rows: 2, | |||||
cols: 60, | |||||
placeholder: "Optional. Do not sign." | |||||
}) | |||||
] | |||||
]); | |||||
this._on_date_change(); | |||||
}); | |||||
}; |
@@ -0,0 +1,39 @@ | |||||
// @TFDCLERK_HEADER_START@ | |||||
/* | |||||
Copyright (C) 2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights to | |||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |||||
of the Software, and to permit persons to whom the Software is furnished to do | |||||
so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. | |||||
*/ | |||||
// @TFDCLERK_HEADER_END@ | |||||
Date.prototype.toDatePickerFormat = function() { | |||||
return this.toISOString().slice(0, 10); | |||||
}; | |||||
Date.prototype.toLogDateFormat = function() { | |||||
if (isNaN(this.valueOf())) | |||||
return null; | |||||
var months = [ | |||||
"January", "February", "March", "April", "May", "June", "July", | |||||
"August", "September", "October", "November", "December"]; | |||||
var month = months[this.getUTCMonth()]; | |||||
return this.getUTCFullYear() + " " + month + " " + this.getUTCDate(); | |||||
}; |
@@ -50,657 +50,17 @@ var is_tfd_page = function() { | |||||
if (is_tfd_page()) { | if (is_tfd_page()) { | ||||
mw.loader.using(dependencies, function() { | mw.loader.using(dependencies, function() { | ||||
/* Main script starts here */ | |||||
/* Main script starts here */ | |||||
TFDClerk = { | |||||
version: "@TFDCLERK_VERSION@", | |||||
script_url: "https://en.wikipedia.org/wiki/User:The_Earwig/tfdclerk.js", | |||||
author_url: "https://en.wikipedia.org/wiki/User_talk:The_Earwig", | |||||
github_url: "https://github.com/earwig/tfdclerk", | |||||
tfds: [], | |||||
@TFDCLERK_INCLUDE:main.js@ | |||||
@TFDCLERK_INCLUDE:util.js@ | |||||
@TFDCLERK_INCLUDE:tfd.js@ | |||||
@TFDCLERK_INCLUDE:tfd_close.js@ | |||||
@TFDCLERK_INCLUDE:tfd_relist.js@ | |||||
_api: new mw.Api(), | |||||
_sysop: $.inArray("sysop", mw.config.get("wgUserGroups")) >= 0 | |||||
// TODO: access time? | |||||
}; | |||||
TFD = function(id, head) { | |||||
this.id = id; | |||||
this.head = head; | |||||
this.box = null; | |||||
this._guard = false; | |||||
this._submit_blockers = []; | |||||
this._wikitext = undefined; | |||||
this._wikitext_callbacks = []; | |||||
}; | |||||
TFDClerk.api_get = function(tfd, params, done, fail, always) { | |||||
TFDClerk._api.get(params) | |||||
.done(function(data) { | |||||
if (done) | |||||
done.call(tfd, data); | |||||
}) | |||||
.fail(function(error) { | |||||
if (fail) | |||||
fail.call(tfd, error); | |||||
else | |||||
tfd._error("API query failure", error); | |||||
}) | |||||
.always(function() { | |||||
if (always) | |||||
always.call(tfd); | |||||
}); | |||||
}; | |||||
TFDClerk.install = function() { | |||||
$("h4").each(function(i, elem) { | |||||
var head = $(elem); | |||||
if (head.next().hasClass("tfd-closed")) | |||||
return; | |||||
if (head.find(".mw-editsection").length == 0) | |||||
return; | |||||
var tfd = new TFD(TFDClerk.tfds.length, head); | |||||
TFDClerk.tfds.push(tfd); | |||||
tfd.add_hooks(); | |||||
}); | |||||
}; | |||||
Date.prototype.toDatePickerFormat = function() { | |||||
return this.toISOString().slice(0, 10); | |||||
} | |||||
Date.prototype.toLogDateFormat = function() { | |||||
if (isNaN(this.valueOf())) | |||||
return null; | |||||
var months = [ | |||||
"January", "February", "March", "April", "May", "June", "July", | |||||
"August", "September", "October", "November", "December"]; | |||||
var month = months[this.getUTCMonth()]; | |||||
return this.getUTCFullYear() + " " + month + " " + this.getUTCDate(); | |||||
} | |||||
TFD.prototype._block_submit = function(reason) { | |||||
if (this._submit_blockers.indexOf(reason) < 0) | |||||
this._submit_blockers.push(reason); | |||||
this.box.find(".tfdclerk-submit").prop("disabled", true); | |||||
}; | |||||
TFD.prototype._unblock_submit = function(reason) { | |||||
var index = this._submit_blockers.indexOf(reason); | |||||
if (index >= 0) | |||||
this._submit_blockers.splice(index, 1); | |||||
if (this._submit_blockers.length == 0) | |||||
this.box.find(".tfdclerk-submit").prop("disabled", false); | |||||
}; | |||||
TFD.prototype._error = function(msg, extra) { | |||||
var elem = $("<span/>", { | |||||
html: "<strong>Error:</strong> " + (extra ? msg + ": " : msg), | |||||
style: "color: #A00;" | |||||
}); | |||||
if (extra) | |||||
elem.append($("<span/>", { | |||||
text: extra, | |||||
style: "font-family: monospace;" | |||||
})); | |||||
var contact = $("<a/>", { | |||||
href: TFDClerk.author_url, | |||||
title: TFDClerk.author_url.split("/").pop().replace(/_/g, " "), | |||||
text: "contact me" | |||||
}), file_bug = $("<a/>", { | |||||
href: TFDClerk.github_url, | |||||
title: TFDClerk.github_url.split("/").splice(-2).join("/"), | |||||
text: "file a bug report" | |||||
}); | |||||
elem.append($("<br/>")) | |||||
.append($("<small/>", { | |||||
html: "This may be caused by an edit conflict or other " + | |||||
"intermittent problem. Try refreshing the page. If the error " + | |||||
"persists, you can " + contact.prop("outerHTML") + " or " + | |||||
file_bug.prop("outerHTML") + "." | |||||
})); | |||||
elem.insertAfter(this.box.find("h5")); | |||||
this._block_submit("error"); | |||||
}; | |||||
TFD.prototype._get_discussion_page = function() { | |||||
var url = this.head.find(".mw-editsection a").first().prop("href"); | |||||
var match = url.match(/title=(.*?)(\&|$)/); | |||||
return match ? match[1] : null; | |||||
}; | |||||
TFD.prototype._get_section_number = function() { | |||||
var url = this.head.find(".mw-editsection a").first().prop("href"); | |||||
var match = url.match(/section=(.*?)(\&|$)/); | |||||
return match ? match[1] : null; | |||||
}; | |||||
TFD.prototype._with_content = function(callback) { | |||||
var title = this._get_discussion_page(), | |||||
section = this._get_section_number(); | |||||
if (title === null || section === null) | |||||
return this._error("couldn't find discussion section"); | |||||
if (this._wikitext !== undefined) { | |||||
if (this._wikitext === null) | |||||
this._wikitext_callbacks.push(callback); | |||||
else | |||||
callback.call(this, this._wikitext); | |||||
return; | |||||
} | |||||
this._wikitext = null; | |||||
TFDClerk.api_get(this, { | |||||
action: "query", | |||||
prop: "revisions", | |||||
rvprop: "content", | |||||
indexpageids: "", | |||||
rvsection: section, | |||||
titles: title | |||||
}, function(data) { | |||||
var pageid = data.query.pageids[0]; | |||||
var content = data.query.pages[pageid].revisions[0]["*"]; | |||||
this._wikitext = content; | |||||
callback.call(this, content); | |||||
for (var i in this._wikitext_callbacks) | |||||
this._wikitext_callbacks[i].call(this, content); | |||||
}, null, function() { | |||||
this._wikitext_callbacks = []; | |||||
}); | |||||
}; | |||||
TFD.prototype._remove_option_box = function() { | |||||
this.box.remove(); | |||||
this.box = null; | |||||
this._guard = false; | |||||
this._submit_blockers = []; | |||||
}; | |||||
TFD.prototype._add_option_box = function(verb, title, callback, options) { | |||||
var self = this; | |||||
this.box = $("<div/>", { | |||||
id: "tfdclerk-" + verb + "-box-" + this.id, | |||||
addClass: "tfdclerk-" + verb + "-box" | |||||
}) | |||||
.css("position", "relative") | |||||
.css("border", "1px solid #AAA") | |||||
.css("color", "#000") | |||||
.css("background-color", "#F9F9F9") | |||||
.css("margin", "0.5em 0") | |||||
.css("padding", "1em") | |||||
.append($("<div/>") | |||||
.css("position", "absolute") | |||||
.css("right", "1em") | |||||
.css("top", "0.5em") | |||||
.css("font-size", "75%") | |||||
.css("color", "#777") | |||||
.append($("<a/>", { | |||||
href: TFDClerk.script_url, | |||||
title: "tfdclerk.js", | |||||
text: "tfdclerk.js" | |||||
})) | |||||
.append($("<span/>", {text: " version " + TFDClerk.version}))) | |||||
.append($("<h5/>", { | |||||
text: title, | |||||
style: "margin: 0; padding: 0 0 0.25em 0;" | |||||
})); | |||||
options.call(this); | |||||
this.box.append($("<button/>", { | |||||
text: verb.charAt(0).toUpperCase() + verb.slice(1), | |||||
addClass: "tfdclerk-submit mw-ui-button mw-ui-progressive", | |||||
style: "margin-right: 0.5em;", | |||||
disabled: this._submit_blockers.length > 0, | |||||
click: function() { | |||||
self._block_submit("submitting"); | |||||
self.box.find(".tfdclerk-cancel").prop("disabled", true); | |||||
callback.call(self); | |||||
} | |||||
})) | |||||
.append($("<button/>", { | |||||
text: "Cancel", | |||||
addClass: "tfdclerk-cancel mw-ui-button", | |||||
click: function() { self._remove_option_box(); } | |||||
})) | |||||
.insertAfter(this.head); | |||||
}; | |||||
TFD.prototype._add_option_table = function(options) { | |||||
var table = $("<table/>", {style: "border-spacing: 0;"}); | |||||
$.each(options, function(i, opt) { | |||||
table.append($("<tr/>") | |||||
.append( | |||||
$("<td/>", { | |||||
style: "padding-bottom: 0.75em; padding-right: 0.5em;" | |||||
}).append(opt[0])) | |||||
.append( | |||||
$("<td/>", { | |||||
style: "padding-bottom: 0.75em;" | |||||
}).append(opt[1])) | |||||
); | |||||
}); | |||||
this.box.append(table); | |||||
}; | |||||
TFD.prototype._build_loading_node = function(node, text) { | |||||
return $("<" + node + "/>", { | |||||
text: text + "...", | |||||
style: "font-style: italic; color: #777;" | |||||
}); | |||||
}; | |||||
TFD.prototype._do_close = function() { | |||||
// TODO | |||||
// rough mockup: | |||||
// - post-submit ui updates | |||||
// - result options -> result string | |||||
// - disable comments box | |||||
// - collapse/disable/fix actions | |||||
// - add progress info area | |||||
// - "Closing discussion..." | |||||
// - add discussion close tags with result and comment, optional nac | |||||
// - " done ([[diff]])" | |||||
// - interface for closing each template | |||||
// - replace gray buttons with progressive refresh button | |||||
}; | |||||
TFD.prototype._do_relist = function() { | |||||
// TODO | |||||
// rough mockup: | |||||
// - post-submit ui updates | |||||
// - date input box -> link to new discussion log page | |||||
// - disable comments box | |||||
// - add progress info area | |||||
// - "Adding discussion to new date..." | |||||
// - add discussion to new date, plus relist template | |||||
// - " done ([[diff]])" | |||||
// - "Removing discussion from old date..." | |||||
// - replace contents of section with {{relisted}} | |||||
// - " done ([[diff]])" | |||||
// - "Updating discussion link on template(s):\n* [[Template:A]]..." | |||||
// - update |link param of {{template for discussion}} | |||||
// - " done ([[diff]])" | |||||
// - replace gray buttons with progressive refresh button | |||||
}; | |||||
TFD.prototype._is_merge = function() { | |||||
return this.head.nextUntil("h4").filter("p").first().find("b") | |||||
.text() == "Propose merging"; | |||||
}; | |||||
TFD.prototype._get_close_action_choices = function() { | |||||
// TODO | |||||
return [{ | |||||
id: "none", | |||||
name: "Do nothing", | |||||
help: "The script will not modify this template or pages related to " + | |||||
"it. You will need to carry out the closure yourself.", | |||||
on_select: null, | |||||
on_close: null, | |||||
default: true | |||||
}, { | |||||
id: "holding-cell", | |||||
name: "Move to holding cell", | |||||
help: "The script will add {{being deleted}} to the template and " + | |||||
"add an entry to the holding cell ([[WP:TFD/H]]).", | |||||
on_select: null, | |||||
on_close: null | |||||
}]; | |||||
}; | |||||
TFD.prototype._on_close_result_change = function() { | |||||
this._unblock_submit("no-close-result"); | |||||
// TODO: possibly disable/enable individual close actions | |||||
}; | |||||
TFD.prototype._build_close_results = function() { | |||||
if (this._is_merge()) | |||||
var choices = ["Merge", "Do not merge", "No consensus", "Other"]; | |||||
else | |||||
var choices = ["Delete", "Keep", "Redirect", "No consensus", "Other"]; | |||||
var self = this; | |||||
var elems = $("<div/>"); | |||||
$("<label/>").append($("<input/>", { | |||||
name: "result-speedy", | |||||
type: "checkbox", | |||||
value: "true" | |||||
})).append($("<span/>", { | |||||
text: "Speedy", | |||||
style: "margin: 0 1.25em 0 0.25em;" | |||||
})).appendTo(elems); | |||||
$.each(choices, function(i, choice) { | |||||
$("<label/>").append($("<input/>", { | |||||
name: "result", | |||||
type: "radio", | |||||
value: choice.toLowerCase(), | |||||
change: function() { self._on_close_result_change(); } | |||||
})).append($("<span/>", { | |||||
text: choice, | |||||
style: "margin: 0 1.25em 0 0.25em;" | |||||
})).appendTo(elems); | |||||
}); | |||||
var other = elems.children().last(); | |||||
other.find("span").text("Other:").css("margin", "0 0.25em"); | |||||
other.append($("<input/>", { | |||||
name: "result-other", | |||||
type: "text" | |||||
})); | |||||
this._block_submit("no-close-result"); | |||||
return elems; | |||||
}; | |||||
TFD.prototype._on_backlink_summary = function(page, tlinfo, ntrans, nmtrans, | |||||
nlinks) { | |||||
tlinfo.empty().append($("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl("Special:WhatLinksHere/" + page, { | |||||
namespace: "", | |||||
hidelinks: 1, | |||||
hideredirs: 1, | |||||
hidetrans: 0 | |||||
}), | |||||
title: "Transclusions of " + page, | |||||
text: ntrans + " transclusions" | |||||
}))); | |||||
if (ntrans != 0) | |||||
tlinfo.append($("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl("Special:WhatLinksHere/" + page, { | |||||
namespace: 0, | |||||
hidelinks: 1, | |||||
hideredirs: 1, | |||||
hidetrans: 0 | |||||
}), | |||||
title: "Mainspace transclusions of " + page, | |||||
text: nmtrans + " in mainspace" | |||||
}))); | |||||
if (nlinks != 0) | |||||
tlinfo.append($("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl("Special:WhatLinksHere/" + page, { | |||||
namespace: "", | |||||
hidelinks: 0, | |||||
hideredirs: 0, | |||||
hidetrans: 1 | |||||
}), | |||||
title: "Mainspace links to " + page, | |||||
text: nlinks + " mainspace links" | |||||
}))); | |||||
}; | |||||
TFD.prototype._load_backlink_summary = function(page, tlinfo) { | |||||
var limit = TFDClerk._sysop ? 5000 : 500; | |||||
TFDClerk.api_get(this, { | |||||
action: "query", | |||||
list: "embeddedin|backlinks", | |||||
eititle: page, | |||||
eilimit: limit, | |||||
bltitle: page, | |||||
blnamespace: "0|10", | |||||
bllimit: limit, | |||||
blredirect: "" | |||||
}, function(data) { | |||||
var ntrans = data.query.embeddedin.length; | |||||
var nmtrans = data.query.embeddedin.filter(function(pg) { | |||||
return pg.ns == 0; | |||||
}).length; | |||||
var nlinks = data.query.backlinks.reduce(function(acc, pg) { | |||||
var c = 0; | |||||
if (pg.ns == 0) | |||||
c++; | |||||
if (pg.redirlinks) | |||||
c += pg.redirlinks.filter(function(rl) { | |||||
return rl.ns == 0; | |||||
}).length; | |||||
return acc + c; | |||||
}, 0); | |||||
if (data["continue"]) { | |||||
if (data["continue"].eicontinue) { | |||||
ntrans += "+"; | |||||
nmtrans += "+"; | |||||
} | |||||
if (data["continue"].blcontinue) | |||||
nlinks += "+"; | |||||
} | |||||
this._on_backlink_summary(page, tlinfo, ntrans, nmtrans, nlinks); | |||||
}); | |||||
}; | |||||
TFD.prototype._build_close_action_entry = function(page) { | |||||
var self = this; | |||||
var redlink = this.head.nextUntil("h4").filter("ul").first() | |||||
.find("a").filter(function() { return $(this).text() == page; }) | |||||
.hasClass("new"); | |||||
var tlinfo = $("<ul/>", {style: "display: inline;"}) | |||||
.append(this._build_loading_node("li", "Fetching transclusions")); | |||||
this._load_backlink_summary(page, tlinfo); | |||||
var select_extra = $("<span/>"); | |||||
var help = $("<abbr/>", {text: "?"}); | |||||
var select = $("<select/>", { | |||||
disabled: redlink, | |||||
change: function() { | |||||
var option = select.find("option:selected"); | |||||
help.prop("title", option.data("help")); | |||||
if (option.data("select")) | |||||
option.data("select").call(self, select_extra); | |||||
} | |||||
}); | |||||
$.each(this._get_close_action_choices(), function(i, choice) { | |||||
select.append($("<option/>", { | |||||
value: choice.id, | |||||
text: choice.name, | |||||
selected: choice.default | |||||
}).data({ | |||||
help: choice.help, | |||||
select: choice.on_select, | |||||
close: choice.on_close | |||||
})); | |||||
if (choice.default) | |||||
help.prop("title", choice.help); | |||||
}); | |||||
return $("<li/>").append($("<a/>", { | |||||
href: mw.util.getUrl(page), | |||||
title: page, | |||||
text: page, | |||||
addClass: redlink ? "new" : "", | |||||
style: "font-weight: bold;" | |||||
})).append($("<span/>", {text: ": "})) | |||||
.append(select) | |||||
.append(select_extra) | |||||
.append($("<span/>", {text: " ("})) | |||||
.append(help) | |||||
.append($("<span/>", {text: ") ("})) | |||||
.append($("<div/>", {addClass: "hlist", style: "display: inline;"}) | |||||
.append(tlinfo)) | |||||
.append($("<span/>", {text: ")"})); | |||||
}; | |||||
TFD.prototype._add_close_actions = function() { | |||||
this._block_submit("add-close-actions"); | |||||
this._with_content(function(content) { | |||||
var regex = /\*\s*\{\{tfd links\|(.*?)(\||\}\})/gi, | |||||
match = regex.exec(content); | |||||
if (match === null) | |||||
return this._error("no templates found in section"); | |||||
var list = $("<ul/>", {style: "margin: 0 0 0 1em;"}); | |||||
do { | |||||
var page = "Template:" + match[1]; | |||||
this._build_close_action_entry(page).appendTo(list); | |||||
} while ((match = regex.exec(content)) !== null); | |||||
this.box.find(".tfdclerk-actions").empty().append(list); | |||||
this._unblock_submit("add-close-actions"); | |||||
}); | |||||
}; | |||||
TFD.prototype._get_relist_date = function() { | |||||
return new Date(this.box.find(".tfdclerk-date").val()).toLogDateFormat(); | |||||
}; | |||||
TFD.prototype._on_date_change = function() { | |||||
var date = this._get_relist_date(); | |||||
var title = "Wikipedia:Templates for discussion/Log/" + date; | |||||
var info = this.box.find(".tfdclerk-discussion-info"); | |||||
info.empty(); | |||||
if (mw.config.get("wgTitle") == "Templates for discussion") | |||||
var this_date = this.head.prevAll().filter("h3").first().find("a") | |||||
.prop("title").split("/Log/")[1]; | |||||
else | |||||
var this_date = mw.config.get("wgTitle").split("/Log/")[1]; | |||||
if (date == null || date == this_date) { | |||||
this._block_submit("bad-date"); | |||||
info.append($("<span/>", { | |||||
text: date == this_date ? "Same as current date" : "Invalid date", | |||||
style: "color: #A00;" | |||||
})); | |||||
return; | |||||
} | |||||
this._unblock_submit("bad-date"); | |||||
this._block_submit("checking-discussion"); | |||||
info.append(this._build_loading_node("span", "Checking discussion")); | |||||
TFDClerk.api_get(this, { | |||||
action: "query", | |||||
indexpageids: "", | |||||
titles: title | |||||
}, function(data) { | |||||
this._unblock_submit("checking-discussion"); | |||||
info.empty().append($("<span/>", {text: "Log: "})).append($("<a/>", { | |||||
href: mw.util.getUrl(title), | |||||
title: title, | |||||
text: date, | |||||
addClass: data.query.pageids[0] < 0 ? "new" : "" | |||||
})); | |||||
if (date == (new Date().toLogDateFormat())) | |||||
info.append($("<span/>", {text: " (today)"})); | |||||
}); | |||||
}; | |||||
TFD.prototype.close = function() { | |||||
if (this._guard) | |||||
return; | |||||
this._guard = true; | |||||
this._add_option_box("close", "Closing discussion", this._do_close, | |||||
function() { | |||||
this._add_option_table([ | |||||
[ | |||||
$("<span/>", {text: "Result:"}), | |||||
this._build_close_results() | |||||
], | |||||
[ | |||||
$("<label/>", { | |||||
for: "tfdclerk-comments-" + this.id, | |||||
text: "Comments:" | |||||
}), | |||||
$("<textarea/>", { | |||||
id: "tfdclerk-comments-" + this.id, | |||||
name: "comments", | |||||
rows: 2, | |||||
cols: 60, | |||||
placeholder: "Optional. Do not sign." | |||||
}) | |||||
], | |||||
[ | |||||
$("<span/>", {text: "Actions:"}), | |||||
$("<div/>", {addClass: "tfdclerk-actions"}) | |||||
.append(this._build_loading_node("span", "Fetching")), | |||||
] | |||||
]); | |||||
this._add_close_actions(); | |||||
}); | |||||
}; | |||||
TFD.prototype.relist = function() { | |||||
var self = this; | |||||
if (this._guard) | |||||
return; | |||||
this._guard = true; | |||||
this._add_option_box("relist", "Relisting discussion", this._do_relist, | |||||
function() { | |||||
this._add_option_table([ | |||||
[ | |||||
$("<label/>", { | |||||
for: "tfdclerk-date-" + this.id, | |||||
text: "New date:" | |||||
}), | |||||
$("<input/>", { | |||||
id: "tfdclerk-date-" + this.id, | |||||
addClass: "tfdclerk-date", | |||||
name: "date", | |||||
type: "date", | |||||
value: new Date().toDatePickerFormat(), | |||||
change: function() { self._on_date_change(); } | |||||
}).add($("<span/>", { | |||||
addClass: "tfdclerk-discussion-info", | |||||
style: "margin-left: 0.5em; vertical-align: middle;" | |||||
})) | |||||
], | |||||
[ | |||||
$("<label/>", { | |||||
for: "tfdclerk-comments-" + this.id, | |||||
text: "Comments:" | |||||
}), | |||||
$("<textarea/>", { | |||||
id: "tfdclerk-comments-" + this.id, | |||||
name: "comments", | |||||
rows: 2, | |||||
cols: 60, | |||||
placeholder: "Optional. Do not sign." | |||||
}) | |||||
] | |||||
]); | |||||
this._on_date_change(); | |||||
}); | |||||
}; | |||||
TFD.prototype._build_hook = function(verb, callback) { | |||||
var self = this; | |||||
return $("<span/>", {style: "margin-left: 1em;"}) | |||||
.append($("<span/>", {addClass: "mw-editsection-bracket", text: "["})) | |||||
.append($("<a/>", { | |||||
href: "#", | |||||
text: verb, | |||||
title: "tfdclerk: " + verb + " discussion", | |||||
click: function() { | |||||
callback.call(self); | |||||
return false; | |||||
} | |||||
})) | |||||
.append($("<span/>", {addClass: "mw-editsection-bracket", text: "]"})); | |||||
}; | |||||
TFD.prototype.add_hooks = function() { | |||||
$("<span/>", {addClass: "tfdclerk-hooks"}) | |||||
.append(this._build_hook("close", this.close)) | |||||
.append(this._build_hook("relist", this.relist)) | |||||
.appendTo(this.head.find(".mw-editsection")); | |||||
}; | |||||
$(TFDClerk.install); | |||||
$(TFDClerk.install); | |||||
/* Main script ends here */ | |||||
/* Main script ends here */ | |||||
}); | }); | ||||
} | } | ||||