| @@ -22,6 +22,8 @@ | |||||
| from os import expanduser | from os import expanduser | ||||
| import re | import re | ||||
| from threading import RLock | |||||
| from time import sleep, time | |||||
| import oursql | import oursql | ||||
| @@ -46,9 +48,13 @@ class DRNClerkBot(Task): | |||||
| def setup(self): | def setup(self): | ||||
| """Hook called immediately after the task is loaded.""" | """Hook called immediately after the task is loaded.""" | ||||
| cfg = self.config.tasks.get(self.name, {}) | cfg = self.config.tasks.get(self.name, {}) | ||||
| # Set some wiki-related attributes: | |||||
| self.title = cfg.get("page", "Wikipedia:Dispute resolution noticeboard") | self.title = cfg.get("page", "Wikipedia:Dispute resolution noticeboard") | ||||
| default_summary = "Updating $3 cases for the [[WP:DRN|dispute resolution noticeboard]]." | |||||
| self.summary = self.make_summary(cfg.get("summary", default_summary)) | |||||
| # Templates used in chart generation: | |||||
| # Templates used: | |||||
| templates = cfg.get("templates", {}) | templates = cfg.get("templates", {}) | ||||
| self.tl_status = templates.get("status", "DR case status") | self.tl_status = templates.get("status", "DR case status") | ||||
| self.tl_notify_party = templates.get("notifyParty", "DRN-notice") | self.tl_notify_party = templates.get("notifyParty", "DRN-notice") | ||||
| @@ -61,16 +67,47 @@ class DRNClerkBot(Task): | |||||
| kwargs = cfg.get("sql", {}) | kwargs = cfg.get("sql", {}) | ||||
| kwargs["read_default_file"] = expanduser("~/.my.cnf") | kwargs["read_default_file"] = expanduser("~/.my.cnf") | ||||
| self.conn_data = kwargs | self.conn_data = kwargs | ||||
| self.db_access_lock = Lock() | |||||
| self.db_access_lock = RLock() | |||||
| def run(self, **kwargs): | def run(self, **kwargs): | ||||
| """Entry point for a task event.""" | """Entry point for a task event.""" | ||||
| if not self.db_access_lock.acquire(False): # Non-blocking | |||||
| self.logger.info("A job is already ongoing; aborting") | |||||
| return | |||||
| with self.db_access_lock: | with self.db_access_lock: | ||||
| self.logger.info(u"Starting update to [[{0}]]".format(self.title)) | |||||
| start = time() | |||||
| conn = oursql.connect(**self.conn_data) | conn = oursql.connect(**self.conn_data) | ||||
| cases = read_database(conn) | cases = read_database(conn) | ||||
| page = self.bot.wiki.get_site().get_page(self.title) | page = self.bot.wiki.get_site().get_page(self.title) | ||||
| text = page.get() | text = page.get() | ||||
| current = read_page(cases, text) | |||||
| read_page(conn, cases, text) | |||||
| # Work! | |||||
| # Send messages! | |||||
| self.save(page, cases) | |||||
| def save(self, page, cases): | |||||
| newtext = text = page.get() | |||||
| counter = 0 | |||||
| for case in cases: | |||||
| if case.old != case.body: | |||||
| newtext = newtext.replace(case.old, case.body) | |||||
| counter += 1 | |||||
| worktime = time() - start | |||||
| if worktime < 60: | |||||
| sleep(60 - worktime) | |||||
| page.reload() | |||||
| if page.get() != text: | |||||
| log = "Someone has edited the page while we were working; restarting" | |||||
| self.logger.warn(log) | |||||
| return self.run() | |||||
| summary = self.summary.replace("$3", str(counter)) | |||||
| page.edit(text, summary, minor=False, bot=True) | |||||
| self.logger.info(u"Saved page [[{0}]]".format(page.title)) | |||||
| def read_database(self, conn): | def read_database(self, conn): | ||||
| """Return a list of _Cases from the database.""" | """Return a list of _Cases from the database.""" | ||||
| @@ -82,24 +119,42 @@ class DRNClerkBot(Task): | |||||
| cases.append(_Case(id_, title, status)) | cases.append(_Case(id_, title, status)) | ||||
| return cases | return cases | ||||
| def read_page(self, cases, text): | |||||
| def read_page(self, conn, cases, text): | |||||
| """Read the noticeboard content and update the list of _Cases.""" | """Read the noticeboard content and update the list of _Cases.""" | ||||
| tl_status_esc = re.escape(self.tl_status) | |||||
| split = re.split("(^==\s*[^=]+?\s*==$)", text, flags=re.M|re.U) | split = re.split("(^==\s*[^=]+?\s*==$)", text, flags=re.M|re.U) | ||||
| cases = [] | |||||
| case = None | |||||
| for item in split: | |||||
| if item.startswith("=="): | |||||
| if case: | |||||
| cases.append(case) | |||||
| case = _Case() | |||||
| case.title = item[2:-2].strip() | |||||
| else: | |||||
| templ = re.escape(self.tl_status) | |||||
| if case and re.match("\s*\{\{" + templ, item, re.U): | |||||
| case.body = case.old_body = item | |||||
| case.status = self.read_status(body) | |||||
| if case: | |||||
| cases.append(case) | |||||
| for i in xrange(len(split)): | |||||
| if i + 1 == len(split): | |||||
| break | |||||
| if not split[i].startswith("=="): | |||||
| continue | |||||
| title = split[i][2:-2].strip() | |||||
| body = old = split[i + 1] | |||||
| if not re.search("\s*\{\{" + tl_status_esc, body, re.U): | |||||
| continue | |||||
| status = self.read_status(body) | |||||
| re_id = "<!-- EarwigBot Case ID \(please don't modify me\): (.*?) -->" | |||||
| try: | |||||
| id_ = re.search(re_id, body).group(1) | |||||
| case = [case for case in cases if case.id == id_][0] | |||||
| except (AttributeError, IndexError): | |||||
| id_ = self.select_next_id(conn) | |||||
| re_id2 = "(\{\{" + tl_status_esc + "(.*?)\}\})(<!-- Case ID \(please don't modify\): .*? -->)?" | |||||
| repl = ur"\1 <!-- Case ID (please don't modify): {0} -->" | |||||
| body = re.sub(re_id2, repl.format(id_), body) | |||||
| case = _Case(id_, title, status) | |||||
| cases.append(case) | |||||
| case.body, case.old = body, old | |||||
| def select_next_id(self, conn): | |||||
| """Return the next incremental ID for a case.""" | |||||
| query = "SELECT MAX(case_id) FROM case" | |||||
| with conn.cursor() as cursor: | |||||
| cursor.execute(query) | |||||
| current = cursor.fetchone()[0] | |||||
| if current: | |||||
| return current + 1 | |||||
| return 1 | |||||
| def read_status(self, body): | def read_status(self, body): | ||||
| """Parse the current status from a case body.""" | """Parse the current status from a case body.""" | ||||
| @@ -130,3 +185,4 @@ class _Case(object): | |||||
| self.status = status | self.status = status | ||||
| self.body = None | self.body = None | ||||
| self.old = None | |||||