User:Robert Ullmann/Rat Patrol

Definition from Wiktionary, the free dictionary
Jump to: navigation, search
Wikipedia has an article on:




This is a patrol program for the English Wiktionary.

It must be run from a sysop/admin account.

It requires Python + the Python Wikipedia framework. It also requires Tkinter, which usually comes with Python.

Updated September 2009, re-written version.


copy everything after the source tag to the end of the page, save as in the pywikipedia directory/folder

# -*- coding: utf-8  -*-

this application patrols the en.wikt

requires some version of the "pywikipedia framework", preferably recent, but need not be updated
all the time (;-), most code is within this file

requires Python 2.6

requires Tkinter (which you should have with Python)

in, make sure one line reads:

sysopnames['wiktionary']['en'] = "(your username)"

then run! will exit more cleanly if you only use the "quit" button, rather than closing the window

the "console" (on Windoze) or the command shell you started from will display lots of log messages
to tell you what it is doing


Edit page       -- open a new tab (or window) in default browser in page edit
Show diffs      -- open a new tab etc showing diffs for revision
Skip user       -- skip edits by this user for 24 hours
Whitelist       -- mark all edits seen by by this user for 24 hours (new or old)
Skip            -- skip this edit for 72 hours (someone else ought to look at it)
Mark            -- mark this edit patrolled (;-)

large edits won't fit in the differences boxes, use the "show diffs" button to see more
the revision list at the bottom will often tell you if a later editor fixed/reverted a bad edit,
which you can then mark


import wikipedia
import sys
import re
import pickle
import time
import xmlreader
import socket
from math import floor
from Tkinter import *
import Queue
import threading
from webbrowser import open_new_tab
from difflib import ndiff, unified_diff
import urllib
import config

def srep(s):
    return repr(u''+s)[2:-1]

plock = threading.Lock()

def log(s):
    with plock: print srep(s)

# slightly odd string syntax in the following is so that it can be saved in wikitext

def unescape(s):
    if '&' not in s: return s
    s = s.replace("&" "lt;", "<")
    s = s.replace("&" "gt;", ">")
    s = s.replace("&" "apos;", "'")
    s = s.replace("&" "quot;", '"')
    s = s.replace("&" "#039;", "'")
    s = s.replace("&" "amp;", "&") # Must be last
    return s

site = None
Quit = False

# ------------------------------------------------------------------------------------------------

class Task():
    def __init__(t, revid = '', pid = '', title = '', user = '', oldid = '',
                    rcid = '', ts = '', summary = ''):
        t.revid = revid = pid
        t.title = title
        t.urlname = urllib.quote(title.encode(site.encoding()))
        t.user = user
        t.oldid = oldid
        t.rcid = rcid
        t.ts = ts
        t.summary = summary
        t.allrevs = None
        t.oldlines = ''
        t.newlines = ''
        t.revlines = ''
        t.reason = ''
        t.done = False

    def __cmp__(s, o):
        # inverse, decreasing [ temp back to increase ]
        if s.ts < o.ts: return -1
        if s.ts > o.ts: return 1
        return 0

# timeout set: a set that elements magically disappear from after a time
# [may need to add thread locks]

from weakref import WeakValueDictionary
from heapq import heappush, heappop

class tmo(float): pass

class timeoutset():

    def __init__(s, timeout):
        s.timeout = timeout
        s.wdict = WeakValueDictionary()
        s.theap = []

    def add(s, key):
        t = tmo(time.clock())
        s.wdict[key] = t
        heappush(s.theap, t)

    def __contains__(s, key):
        while s.theap and s.theap[0] < time.clock() - s.timeout: heappop(s.theap)
        return key in s.wdict

    def __len__(s):
        return len(s.theap)

# all active tasks, not in skip list, key is revid
active = WeakValueDictionary()

# tasks skipped for a few days, values are revids
skipped = timeoutset(96 * 3600)

# whitelisted users
whitelist = timeoutset(24 * 3600)

# users to skip for a while
skipusers = timeoutset(24 * 3600)

# queues: tasks, ready to present, ready to mark

tasq = Queue.PriorityQueue()
readyq = Queue.Queue()
patrolq = Queue.Queue()

# prompt "queue": one task rechecked and ready to display
promptq = Queue.Queue(1)

# check a task to see where it should go, used at several steps

def checktask(task):

    if task.done: return None # drops task from active presumably

    if task.user in whitelist:
        task.reason = 'user ' + task.user + ' in whitelist'
        return None

    if task.title in whitelistpages:
        task.reason = 'page in whitelist'
        return None

    if task.user in skipusers:
        stat("Skipped", len(skipped))
        return None

    return task # next step as normal

# fairly static list of white listed pages (some also annoying to load lots of revs, very big ;-)

whitelistpages = [  'Wiktionary:Requests for verification',
                    'Wiktionary:Requests for deletion',
                    'Wiktionary:Requests for cleanup',
                    'Wiktionary:Tea room',
                    'Wiktionary:Beer parlour',
                    'Wiktionary:Grease pit',
                    'Wiktionary:Requested entries:English',
                    'Wiktionary:Requested entries:Spanish',
                    'Wiktionary:List of protologisms',
                    'Wiktionary:Translation requests',
                    'Wiktionary:Featured word candidates',
                    'Wiktionary:Information desk',
                    'Wiktionary talk:Sandbox' ]

# ------------------------------------------------------------------------------------------------

# mwapi interface, a few mods here

from StringIO import StringIO
from gzip import GzipFile

# first, our own read url routine, so we can accept gzip, and be much faster:

class MyURLopener(urllib.FancyURLopener):

# since we aren't using the framework 'throttle', do something better
# this is a "tick-tock" timer, shared on all threads
# clicked down each success, up on each network failure of any type

ticktocklock = threading.Lock()
ticktock = 1.0
def getticktock(): 
    global ticktock
    return ticktock

relagged = re.compile(r'<error.*"maxlag".* (\d+) seconds')

def readapi(site, request, sysop = True, nomaxlag = False):
    global ticktocklock, ticktock

    url = "http://" + site.hostname() + "/w/api.php?" + request

    done = False
    nap = 5
    maxl = 5
    maxlag = "&maxlag=%d" % maxl
    if nomaxlag: maxlag = ''

    with ticktocklock:
        ticktock *= 0.95  # is -0.025 if 5 seconds, -1.0 at 20 seconds
        ticktock = max(ticktock, 0.1)
        ticktock = min(ticktock, 20.0)
        if ticktock >= 10.0:
            with plock: print "(mwapi readapi: tick tock is %.1f)" % ticktock
        ticktock -= 1.0   # undo first increment in loop

    while not done:
        ticktock += 1.0   # done w/o lock, race condition is rare, not a serious problem, ignored!
            uo = MyURLopener()
            uo.addheader('Cookie', site.cookies(sysop = sysop) or '')
            uo.addheader('Accept-Encoding', 'gzip')
            f = + maxlag)
            text =
                if 'gzip' in['Content-Encoding']:
                    text = GzipFile(fileobj=StringIO(text)).read()
            except KeyError:
            text = unicode(text, 'UTF-8' , errors = 'ignore')
            done = True
        except Exception, e:
            """ report all errors for now:
            if '10054' in repr(e) and nap < 15:
                continue # quietly
            with plock:
                print "(%s: exception reading API: %s)" % (threading.currentThread().name, repr(e))
            text = ''
            nap = min(nap + nap/2, 300)

        # following is specific to the net I am on; won't break anything for anyone else
        # so don't worry about it; but you can remove this block if desired
        if '<api' not in text and 'NdxICC' in text:
            # silently ignore bad return from Nomadix box
            done = False

        # use MW "server lag" feature to slow when servers are under heavy load:
        mo =
        if mo:
            replag = int(
            with plock: print "(%s: server lagged %s seconds)" % \
                           (threading.currentThread().name, replag)
            # allow more lag the next time
            maxl += max(maxl/4, replag/20)
            maxlag = "&maxlag=%d" % maxl
            # make some progress even when server crocked ...
            if maxl > 600: maxlag = ""
            if maxlag and maxl > 60:
                with plock: print "(mwapi readapi: next with %s)" % maxlag
            # sleep replag if not more than 70
            time.sleep(min(replag, 70))
            done = False

    return text

# ------------------------------------------------------------------------------------------------

# recent changes, stuff into task queue

def readrc():

    # keep track of recently seen revids, they can appear to be un-patrolled in the reply
    # but we've just now done them ...
    seen = timeoutset(7200)

    # use regex. this will break sometimes if API is changed, so we will fix it (:-)

    rerev = re.compile(r'<rc [^>]*title="([^"]*)" rcid="([^"]*)" ' +
                       r'pageid="([^"]*)" revid="([^"]*)" old_revid="([^"]*)" user="([^"]*)"' +
                       r'[^>]*timestamp="([^"]*)" comment="([^"]*)"')

    restar = re.compile(r'rcstart="([^"]*)"')

    nap = 0
    rcstart = '' # start at maxage
    # temp:
    # rcstart = "&rcstart=2009-09-19T00:00:00Z"
    lastr = time.time()

    while not Quit:
        nf = 0

        with plock: print "reading rc"
        rcs = readapi(site, "action=query&list=recentchanges&rcprop=title|ids|user|timestamp|comment" +
                        "&rclimit=max&rcshow=!patrolled&rcdir=newer&format=xml" + rcstart)

        for mo in rerev.finditer(rcs):
             title = unescape(
             rcid =
             pid =
             revid =
             oldid =
             if oldid == "0": oldid = ""
             user = unescape(
             ts =
             summary = unescape(

             if revid in active: continue
             if revid in skipped: continue
             if revid in seen: continue

             # probably a good task
             nf += 1
             task = Task(title=title, user=user, revid=revid, oldid=oldid,
                         rcid=rcid, pid=pid, ts=ts, summary=summary)
             active[revid] = task
             stat("Unpatrolled", len(active))

             task = checktask(task)
             if task: tasq.put(task)

        task = None

        # next timestamp forward (not given when we catch up to current)
        mo =
        if mo:
            rcstart = "&rcstart=" +
            # 40 minutes ago (must be >> max naptime)
            rcstart = '&rcstart=' + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(time.time() - 2400))

        # housekeeping (things that otherwise don't get updated when timed out)
        stat("Whitelist users", len(whitelist))
        stat("Skipped users", len(skipusers))
        stat("Unpatrolled", len(active))

        # every few hours or so, start at beginning again (pick up skips, anything missed)
        if lastr < time.time() - (4 * 3600):
             rcstart = ''
             lastr = time.time()

        # nap time ...
        if nf > 2:
             nap /= 2
             nap = min(nap+20, 350)
        with plock: print "rc found", nf, "next in", nap
        for i in range(nap/5):
             if Quit: break

    with plock: print "recent changes thread ends"
# ------------------------------------------------------------------------------------------------

# read patrol log

def readpl():

    markedothers = 0

    repat = re.compile(r'<item [^>]*title="([^"]*)"[^>]*user="([^"]*)"[^>]*>' +
                       r'\s*<patrol[^>]*prev="([^"]*)" cur="([^"]*)"')

    restar = re.compile(r'lestart="([^"]*)"')

    # start a while ago
    lestart = '&lestart=' + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(time.time() - 2400))

    nap = 10

    while not Quit:

        nf = 0

        if len(active):
            with plock: print "reading patrol log"
            pats = readapi(site, "action=query&list=logevents&letype=patrol&lelimit=max&format=xml" +
                                 "&ledir=newer" + lestart)
        else: pats = '' # little point, eh? sleep some more ...

        for mo in repat.finditer(pats):
            title = unescape(
            user = unescape(
            prev = # oldid
            cur = # revid

            task = None
            if cur in active:
                    task = active[cur]
                except KeyError:
                    pass # race with GC
            if not task or task.done: continue

            with plock: print "rev %s of %s patrolled by %s" % (srep(cur), srep(title), srep(user))
            task.done = True # we have no idea where it is ... (:-)
            markedothers += 1
            stat("Marked by others", markedothers)
            nf += 1

        task = None

        # next timestamp forward (not given when we catch up to current)
        mo =
        if mo:
            lestart = "&lestart=" +
            # 40 minutes ago (must be >> max naptime)
            lestart = '&lestart=' + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(time.time() - 2400))

        if nf: nap /= 2
        else: nap = min(nap+20, 700)
        for i in range(0, nap/5):
             if Quit: break

    with plock: print "read patrol log thread ends"

# ------------------------------------------------------------------------------------------------

# mark edits

def patrol():

    markedbyme = 0
    whitelisted = 0

    while not Quit:
            task = patrolq.get(timeout=20)
        except Queue.Empty:

        if task.done: 
            task = None
            continue # can be marked and patrolled by someone else
        # set here, race with read log, that often picks it before we get reply!
        task.done = True

        resp = site.getUrl("/w/index.php?title=%s&action=markpatrolled&rcid=%s" % \
              (task.urlname, task.rcid), sysop = True)

        if "Marked as patrolled" not in resp:
           log("failed to patrol %s of %s" % (task.rcid, task.title))
           for line in resp.splitlines(): print srep(line)
           task = None
           # will presumably pick it up again eventually, if not marked by another
           # if it did in fact succeed, no matter ...

        if not task.reason:
            markedbyme += 1
            stat("Marked by me", markedbyme)
            with plock: print "patrolled %s of %s, my mark" % (srep(task.revid), srep(task.title))
        else: # presume was marked by whitelisting user or page
            whitelisted += 1
            stat("Whitelisted", whitelisted)
            with plock: 
                print "patrolled %s of %s, %s" % \
                      (srep(task.revid), srep(task.title), srep(task.reason))

        task = None
        stat("Unpatrolled", len(active))

        time.sleep(5) # no hurry

    with plock: print "patrol thread ends"

# ------------------------------------------------------------------------------------------------

# preload, read from task q, write to ready q

def preload():

    rever = re.compile(r'<rev revid="([^"]*)"[^>]*user="([^"]*)"' +
                       r'[^>]*timestamp="([^"]*)"\s*(comment="[^"]*"|)[^>]*>(.*?)</rev>', re.S)

    # cache of previously read revisions, kept as long as some other task has them
    # key is title, value is other task (can't just be unicode string, must be object)
    revcache = WeakValueDictionary()

    while not Quit:
            task = tasq.get(timeout=20)
        except Queue.Empty:
        task = checktask(task)
        if not task: continue

        log('preload for ' + task.title)

        # see if we have revs already

        revs = None
        if task.title in revcache:
                ot = revcache[task.title]
                revs = ot.allrevs # from other task
                ot = None
            except KeyError: pass

        if not revs:
            with plock: print "reading 20 revs for", srep(task.title)
            revs = readapi(site,
                 "&titles=" + task.urlname + "&rvlimit=20")

        # now we want to see if we have enough; maybe not, and maybe stale
        if 'revid="' + task.revid + '"' not in revs: revs = ''
        if task.oldid and 'revid="' + task.oldid + '"' not in revs: revs = ''

        # if not, do it again at 200
        if not revs:
            with plock: print "reading 200 revs for", srep(task.title)
            revs = readapi(site,
                 "&titles=" + task.urlname + "&rvlimit=200")

        # check again!
        if 'revid="' + task.revid + '"' not in revs: revs = ''
        if task.oldid and 'revid="' + task.oldid + '"' not in revs: revs = ''

        if not revs:
             # can happen on page deletes, moves, stuff
             with plock: "can't find needed old revs for %s, skipping task!" % srep(task.title)
             stat("Skipped", len(skipped))
             task = None
             stat("Unpatrolled", len(active))

        task.allrevs = revs
        revcache[task.title] = task
        # now available for other tasks on same title

        # now find the revs we want, and make a list ...
        oldrevtext = ''
        newrevtext = ''
        replinelist = [ ]

        for mo in rever.finditer(revs):
             revid =
             if revid == task.revid: mark = '*'
             else: mark = ' '
             user = unescape(
             ts =
             ts = ts.replace('T', ' ')
             ts = ts.replace('Z', ' ')
             comment = unescape([9:-1])
             text = unescape(

             if len(replinelist) < 10:
                 replinelist.append("%s %s %s: (%s)" % (ts, mark, user, comment))
                 # with plock: print "debug match rev", srep(ts), srep(user), srep(comment)
             elif revid == task.revid:
                 # add to list with ellipsis (;-)
                 replinelist = replinelist[:-2]
                 replinelist.append("       [ ... ]")
                 replinelist.append("%s %s %s: (%s)" % (ts, mark, user, comment))

             if revid == task.revid: newrevtext = text
             if revid == task.oldid: oldrevtext = text

        # should have always been found?
        if not newrevtext:
            with plock: print "what? can't match new revtext in revs?"
            continue # discard task?

        task.revlines = u'\n'.join(replinelist)

        # differences

        """ old
        for delta in ndiff(oldrevtext.splitlines(), newrevtext.splitlines()):
            delta = unescape(delta)
            if delta.startswith('- '):
                task.oldlines += delta[2:] + '\n'
            elif delta.startswith('+ '):
                task.newlines += delta[2:] + '\n'
            # ignore '  ' and '? ' lines, might do something with context later?
        for delta in unified_diff(oldrevtext.splitlines(), newrevtext.splitlines(), n=1):
            delta = unescape(delta)
            if delta.startswith('--'): pass
            elif delta.startswith('++'): pass
            elif delta.startswith('-'):
                task.oldlines += delta[1:] + '\n'
            elif delta.startswith('+'):
                task.newlines += delta[1:] + '\n'
            elif delta.startswith(' '):
                task.oldlines += delta[1:] + '\n'
                task.newlines += delta[1:] + '\n'
            # ignore other lines

        task = None

        nap = readyq.qsize() / 20
        for i in range(nap/5):
            if Quit: break

    with plock: print "preload thread ends"

# ------------------------------------------------------------------------------------------------

# recheck task, just before presenting to user
# reverted edits don't show as patrolled in the log
# and page may be (often is) deleted
# so we look for one row in the RC table to tell us if the edit is still there

# has to run as a thread or it hangs the UI
# takes a task off of the readyq and stuffs it in the prompt "queue" (placeholder for one entry)

# note we can't select on rcid (which would be cool, and obvious), so we look for everything
# with the exact timestamp

reclines = re.compile(r'<rc[^>]*rcid="(\d*)"[^>]*>')

def recheck():

  while not Quit:

    # get a new task:

        task = readyq.get_nowait()
    except Queue.Empty:

    task = checktask(task)
    if not task or task.done:
        task = None

    rcs = readapi(site, "action=query&list=recentchanges&rcprop=ids|patrolled" +
                  "&rclimit=20&format=xml&rcstart=%s&rcend=%s" % (task.ts, task.ts), nomaxlag = True)

    # print srep(rcs)

    for mo in reclines.finditer(rcs):
        if != task.rcid: continue
        if "patrolled" in
            with plock: print "rev %s of %s patrolled by someone" % (srep(task.revid), srep(task.title))
            task = None
            task = None
            while promptq.qsize() > 0: time.sleep(1) # wait for it to be eaten

    # not found?
    if task:
        with plock: print "rev %s of %s apparently deleted" % (srep(task.revid), srep(task.title))
        task = None

  with plock: print "recheck thread ends"

# ------------------------------------------------------------------------------------------------

# now the tkinter stuff:
# could do a fancy class with attributes and methods, but instead keep it simple, just share some stuff

root = None
status = None
statboxes = { }

tkmessages = Queue.Queue()

def stat(n, v): tkmessages.put( (n, v) )

# messages to update stats from other threads

def tkmess():

    while tkmessages.qsize():
            lab, val = tkmessages.get()

        except Queue.Empty:

    root.after(200, tkmess)

# oldest (?) unpatrolled page data

# shared things, just easier this way, all belong to tk run thread
oldboxes = { }
oldedit = None # current task being presented
oldlines = None
newlines = None
revlines = None
showdiffb = None
oldlineslabel = None
newlineslabel = None

def get_next_oldpage():
    global oldboxes, oldedit
    global showdiffb, oldlineslabel, newlineslabel

    oldedit = None


        oldedit = promptq.get_nowait()
    except Queue.Empty:
        root.after(5000, get_next_oldpage)

    oldedit = checktask(oldedit)
    if not oldedit:
        # recall immediately:
        root.after(20, get_next_oldpage)
    if oldedit.oldid:
        oldlineslabel.config(text='Old lines')
        newlineslabel.config(text='New lines')
        showdiffb.config(text='Show diffs')
        newlineslabel.config(text='Page text')
        showdiffb.config(text='Show page')



def mark_edit():

    if not oldedit: return



def show_diff():

    if not oldedit: return

    if oldedit.oldid:
        open_new_tab("" % 
               (oldedit.urlname, oldedit.oldid))
        open_new_tab("" % oldedit.urlname)


def edit_page():

    if not oldedit: return

    open_new_tab("" % oldedit.urlname)


def skip_edit():
    global oldedit

    if not oldedit: return

    stat("Skipped", len(skipped))
    oldedit = None
    stat("Unpatrolled", len(active))


def skip_user():
    global oldedit
    if not oldedit: return

    stat("Skipped users", len(skipusers))

    # continue to skip this edit

def whitelist_user():
    global oldedit
    if not oldedit: return

    stat("Whitelist users", len(whitelist))

    # continue to mark this edit

def rats_quit():
    global Quit

    Quit = True
    log("Quitting ...")


# main program runs tkinter loop

def main():
    global site


    site = wikipedia.getSite("en", "wiktionary")
    site.forceLogin(sysop = True)
    # force this off, sysop is never bot (framework bug)
    config.notify_unflagged_bot = False

    # things shared with subs
    global root, oldlines, newlines, revlines
    global showdiffb, oldlineslabel, newlineslabel

    rct = threading.Thread(target=readrc)
    rct.daemon = True = 'read recent changes'

    plt = threading.Thread(target=readpl)
    plt.daemon = True = 'read patrol log'

    prt = threading.Thread(target=preload)
    prt.daemon = True = 'preload'

    pat = threading.Thread(target=patrol)
    pat.daemon = True = 'mark patrol'

    rkt = threading.Thread(target=recheck)
    rkt.daemon = True = 'recheck revision'

    root = Tk()
    root.title('Rat Patrol')

    font = ('Arial', 10)
    fontb = ('Arial', 10, 'bold')

    # pack from bottom, then left to right at top:

    revlines = Label(root, width=97, height=10, justify=LEFT, anchor=W, font=font, bg='#fff', relief=RIDGE)
    revlines.pack(side=BOTTOM, padx=5, pady=5)

    # button bar

    bbox = Frame(root)
    bbox.pack(side=BOTTOM, fill=X, padx=10, pady=5)

    editpageb = Button(bbox, text="Edit page", width=11, font=font, command=edit_page)

    showdiffb = Button(bbox, text="Show diffs", width=11, font=font, command=show_diff)

    skipuserb = Button(bbox, text="Skip user", width=11, font=font, command=skip_user)

    wluserb = Button(bbox, text="Whitelist", width=11, font=font, command=whitelist_user)

    skipeditb = Button(bbox, text="Skip", width=8, font=font, command=skip_edit)

    markeditb = Button(bbox, text="Mark", width=8, font=font, command=mark_edit)

    quitb = Button(bbox, text='Quit', width=10, font=font, command=rats_quit)

    # differences

    dbox = Frame(root)
    dbox.pack(side=BOTTOM, padx=10, pady=5)

    obox = Frame(dbox)
    oldlineslabel = Label(obox, text="Old lines", width=24, font=fontb, justify=LEFT, anchor=W)
    oldlineslabel.pack(side=TOP, fill=X)
    oldlines = Label(obox, width=48, height=8, justify=LEFT, anchor=W, font=font, bg='#fff', relief=RIDGE)
    oldlines.config(wraplength = oldlines.winfo_reqwidth() - 8)

    nbox = Frame(dbox)
    newlineslabel = Label(nbox, text="New lines", width=24, font=fontb, justify=LEFT, anchor=W)
    newlineslabel.pack(side=TOP, fill=X)
    newlines = Label(nbox, width=48, height=8, justify=LEFT, anchor=W, font=font, bg='#fff', relief=RIDGE)
    newlines.config(wraplength = newlines.winfo_reqwidth() - 8)

    # statistics frame, boxes
    stats = Frame(root)
    stats.pack(side=LEFT, padx=10, pady=5)

    statframes = { }
    statlabels = { }
    for lab in [ 'Unpatrolled', 'Marked by me', 'Marked by others', 'Whitelisted', \
                 'Whitelist users', 'Skipped users', 'Skipped' ]:
        statframes[lab] = Frame(stats)
        statlabels[lab] = Label(statframes[lab], text=lab+':', width=15, font=font, justify=LEFT, anchor=W)
        statboxes[lab] = Label(statframes[lab], text='0', width=10, font=font, justify=RIGHT, anchor=E, bg='#fff', relief=RIDGE)

    ebox = Frame(root)
    ebox.pack(side=LEFT, padx=10, pady=5, fill=X)

    oldframes = { }
    oldlabels = { }
    oldtoplabel = Label(ebox, text="Oldest unpatrolled edit", width=24, font=fontb, justify=LEFT, anchor=W)
    oldtoplabel.pack(side=TOP, fill=X)

    for lab in [ 'Title', 'User', 'Summary' ]:
        oldframes[lab] = Frame(ebox)
        oldlabels[lab] = Label(oldframes[lab], text=lab+':', width=15, font=font, justify=LEFT, anchor=NW)
        oldboxes[lab] = Label(oldframes[lab], text='', width=48, font=font, justify=LEFT, anchor=W, bg='#fff', relief=RIDGE)
    oldlabels['Summary'].config(text='Summary:\n\n\n')  # (hack ;-)
    oldboxes['Summary'].config(height = 4)
    oldboxes['Summary'].config(wraplength = oldboxes['Summary'].winfo_reqwidth() - 8)

    root.after(200, tkmess)
    root.after(200, get_next_oldpage)


if __name__ == "__main__":