Browse Source

Split tfdclerk.js into multiple files.

develop
Ben Kurtovic 8 years ago
parent
commit
28b5e368d2
7 changed files with 812 additions and 655 deletions
  1. +43
    -7
      release.py
  2. +67
    -0
      src/main.js
  3. +230
    -0
      src/tfd.js
  4. +293
    -0
      src/tfd_close.js
  5. +132
    -0
      src/tfd_relist.js
  6. +39
    -0
      src/util.js
  7. +8
    -648
      tfdclerk.js

+ 43
- 7
release.py View File

@@ -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())



+ 67
- 0
src/main.js View File

@@ -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();
});
};

+ 230
- 0
src/tfd.js View File

@@ -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"));
};

+ 293
- 0
src/tfd_close.js View File

@@ -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();
});
};

+ 132
- 0
src/tfd_relist.js View File

@@ -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();
});
};

+ 39
- 0
src/util.js View File

@@ -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();
};

+ 8
- 648
tfdclerk.js View File

@@ -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 */

});
}


Loading…
Cancel
Save