| @@ -37,6 +37,7 @@ import errno | |||
| from getpass import getpass | |||
| from os import chmod, path | |||
| import stat | |||
| import re | |||
| from sys import argv | |||
| import time | |||
| from urllib import urlencode | |||
| @@ -45,8 +46,10 @@ import earwigbot | |||
| import git | |||
| SCRIPT_SITE = "en.wikipedia.org" | |||
| SCRIPT_TEST = "test.wikipedia.org" | |||
| SCRIPT_USER = "The Earwig" | |||
| SCRIPT_FILE = "tfdclerk.js" | |||
| SCRIPT_SDIR = "src" | |||
| COOKIE_FILE = ".cookies" | |||
| REPLACE_TAG = "@TFDCLERK_{tag}@" | |||
| 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__)) | |||
| REPO = git.Repo(SCRIPT_ROOT) | |||
| if len(argv) > 1 and argv[1].lstrip("-").startswith("t"): | |||
| SCRIPT_SITE = "test.wikipedia.org" | |||
| def _is_clean(): | |||
| """ | |||
| 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) | |||
| 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(): | |||
| """ | |||
| Return the complete script. | |||
| @@ -85,6 +107,11 @@ def _get_script(): | |||
| with open(path.join(SCRIPT_ROOT, SCRIPT_FILE), "r") as fp: | |||
| 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 = { | |||
| "VERSION": _get_version(), | |||
| "VERSION_FULL": _get_full_version() | |||
| @@ -111,7 +138,7 @@ def _get_cookiejar(): | |||
| return cookiejar | |||
| def _get_site(): | |||
| def _get_site(site_url=SCRIPT_SITE): | |||
| """ | |||
| 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. | |||
| """ | |||
| 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") | |||
| logged_in_as = site._get_username_from_cookies() | |||
| @@ -133,14 +160,23 @@ def main(): | |||
| """ | |||
| Main entry point for script. | |||
| """ | |||
| if len(argv) > 1 and argv[1].lstrip("-").startswith("c"): | |||
| print(_get_script(), end="") | |||
| return | |||
| if not _is_clean(): | |||
| print("Uncommitted changes in working directory. Stopping.") | |||
| 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( | |||
| page=SCRIPT_PAGE, site=SCRIPT_SITE)) | |||
| page=SCRIPT_PAGE, site=site_url)) | |||
| script = _get_script() | |||
| site = _get_site() | |||
| site = _get_site(site_url) | |||
| page = site.get_page(SCRIPT_PAGE) | |||
| 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()) { | |||
| 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 */ | |||
| }); | |||
| } | |||