diff --git a/release.py b/release.py index 7657ecf..f4f0fe3 100755 --- a/release.py +++ b/release.py @@ -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()) diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..eb9f21a --- /dev/null +++ b/src/main.js @@ -0,0 +1,67 @@ +// @TFDCLERK_HEADER_START@ +/* +Copyright (C) 2015 Ben Kurtovic + +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(); + }); +}; diff --git a/src/tfd.js b/src/tfd.js new file mode 100644 index 0000000..786c38f --- /dev/null +++ b/src/tfd.js @@ -0,0 +1,230 @@ +// @TFDCLERK_HEADER_START@ +/* +Copyright (C) 2015 Ben Kurtovic + +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 = $("", { + html: "Error: " + (extra ? msg + ": " : msg), + style: "color: #A00;" + }); + if (extra) + elem.append($("", { + text: extra, + style: "font-family: monospace;" + })); + + var contact = $("", { + href: TFDClerk.author_url, + title: TFDClerk.author_url.split("/").pop().replace(/_/g, " "), + text: "contact me" + }), file_bug = $("", { + href: TFDClerk.github_url, + title: TFDClerk.github_url.split("/").splice(-2).join("/"), + text: "file a bug report" + }); + elem.append($("
")) + .append($("", { + 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 = $("
", { + 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($("
") + .css("position", "absolute") + .css("right", "1em") + .css("top", "0.5em") + .css("font-size", "75%") + .css("color", "#777") + .append($("", { + href: TFDClerk.script_url, + title: "tfdclerk.js", + text: "tfdclerk.js" + })) + .append($("", {text: " version " + TFDClerk.version}))) + .append($("
", { + text: title, + style: "margin: 0; padding: 0 0 0.25em 0;" + })); + + options.call(this); + this.box.append($("