| #! /usr/bin/env python | |
| """GUI interface to webchecker. | |
| This works as a Grail applet too! E.g. | |
| <APPLET CODE=wcgui.py NAME=CheckerWindow></APPLET> | |
| Checkpoints are not (yet??? ever???) supported. | |
| User interface: | |
| Enter a root to check in the text entry box. To enter more than one root, | |
| enter them one at a time and press <Return> for each one. | |
| Command buttons Start, Stop and "Check one" govern the checking process in | |
| the obvious way. Start and "Check one" also enter the root from the text | |
| entry box if one is present. There's also a check box (enabled by default) | |
| to decide whether actually to follow external links (since this can slow | |
| the checking down considerably). Finally there's a Quit button. | |
| A series of checkbuttons determines whether the corresponding output panel | |
| is shown. List panels are also automatically shown or hidden when their | |
| status changes between empty to non-empty. There are six panels: | |
| Log -- raw output from the checker (-v, -q affect this) | |
| To check -- links discovered but not yet checked | |
| Checked -- links that have been checked | |
| Bad links -- links that failed upon checking | |
| Errors -- pages containing at least one bad link | |
| Details -- details about one URL; double click on a URL in any of | |
| the above list panels (not in Log) will show details | |
| for that URL | |
| Use your window manager's Close command to quit. | |
| Command line options: | |
| -m bytes -- skip HTML pages larger than this size (default %(MAXPAGE)d) | |
| -q -- quiet operation (also suppresses external links report) | |
| -v -- verbose operation; repeating -v will increase verbosity | |
| -t root -- specify root dir which should be treated as internal (can repeat) | |
| -a -- don't check name anchors | |
| Command line arguments: | |
| rooturl -- URL to start checking | |
| (default %(DEFROOT)s) | |
| XXX The command line options (-m, -q, -v) should be GUI accessible. | |
| XXX The roots should be visible as a list (?). | |
| XXX The multipanel user interface is clumsy. | |
| """ | |
| # ' Emacs bait | |
| import sys | |
| import getopt | |
| from Tkinter import * | |
| import tktools | |
| import webchecker | |
| def main(): | |
| try: | |
| opts, args = getopt.getopt(sys.argv[1:], 't:m:qva') | |
| except getopt.error, msg: | |
| sys.stdout = sys.stderr | |
| print msg | |
| print __doc__%vars(webchecker) | |
| sys.exit(2) | |
| webchecker.verbose = webchecker.VERBOSE | |
| webchecker.nonames = webchecker.NONAMES | |
| webchecker.maxpage = webchecker.MAXPAGE | |
| extra_roots = [] | |
| for o, a in opts: | |
| if o == '-m': | |
| webchecker.maxpage = int(a) | |
| if o == '-q': | |
| webchecker.verbose = 0 | |
| if o == '-v': | |
| webchecker.verbose = webchecker.verbose + 1 | |
| if o == '-t': | |
| extra_roots.append(a) | |
| if o == '-a': | |
| webchecker.nonames = not webchecker.nonames | |
| root = Tk(className='Webchecker') | |
| root.protocol("WM_DELETE_WINDOW", root.quit) | |
| c = CheckerWindow(root) | |
| c.setflags(verbose=webchecker.verbose, maxpage=webchecker.maxpage, | |
| nonames=webchecker.nonames) | |
| if args: | |
| for arg in args[:-1]: | |
| c.addroot(arg) | |
| c.suggestroot(args[-1]) | |
| # Usually conditioned on whether external links | |
| # will be checked, but since that's not a command | |
| # line option, just toss them in. | |
| for url_root in extra_roots: | |
| # Make sure it's terminated by a slash, | |
| # so that addroot doesn't discard the last | |
| # directory component. | |
| if url_root[-1] != "/": | |
| url_root = url_root + "/" | |
| c.addroot(url_root, add_to_do = 0) | |
| root.mainloop() | |
| class CheckerWindow(webchecker.Checker): | |
| def __init__(self, parent, root=webchecker.DEFROOT): | |
| self.__parent = parent | |
| self.__topcontrols = Frame(parent) | |
| self.__topcontrols.pack(side=TOP, fill=X) | |
| self.__label = Label(self.__topcontrols, text="Root URL:") | |
| self.__label.pack(side=LEFT) | |
| self.__rootentry = Entry(self.__topcontrols, width=60) | |
| self.__rootentry.pack(side=LEFT) | |
| self.__rootentry.bind('<Return>', self.enterroot) | |
| self.__rootentry.focus_set() | |
| self.__controls = Frame(parent) | |
| self.__controls.pack(side=TOP, fill=X) | |
| self.__running = 0 | |
| self.__start = Button(self.__controls, text="Run", command=self.start) | |
| self.__start.pack(side=LEFT) | |
| self.__stop = Button(self.__controls, text="Stop", command=self.stop, | |
| state=DISABLED) | |
| self.__stop.pack(side=LEFT) | |
| self.__step = Button(self.__controls, text="Check one", | |
| command=self.step) | |
| self.__step.pack(side=LEFT) | |
| self.__cv = BooleanVar(parent) | |
| self.__cv.set(self.checkext) | |
| self.__checkext = Checkbutton(self.__controls, variable=self.__cv, | |
| command=self.update_checkext, | |
| text="Check nonlocal links",) | |
| self.__checkext.pack(side=LEFT) | |
| self.__reset = Button(self.__controls, text="Start over", command=self.reset) | |
| self.__reset.pack(side=LEFT) | |
| if __name__ == '__main__': # No Quit button under Grail! | |
| self.__quit = Button(self.__controls, text="Quit", | |
| command=self.__parent.quit) | |
| self.__quit.pack(side=RIGHT) | |
| self.__status = Label(parent, text="Status: initial", anchor=W) | |
| self.__status.pack(side=TOP, fill=X) | |
| self.__checking = Label(parent, text="Idle", anchor=W) | |
| self.__checking.pack(side=TOP, fill=X) | |
| self.__mp = mp = MultiPanel(parent) | |
| sys.stdout = self.__log = LogPanel(mp, "Log") | |
| self.__todo = ListPanel(mp, "To check", self, self.showinfo) | |
| self.__done = ListPanel(mp, "Checked", self, self.showinfo) | |
| self.__bad = ListPanel(mp, "Bad links", self, self.showinfo) | |
| self.__errors = ListPanel(mp, "Pages w/ bad links", self, self.showinfo) | |
| self.__details = LogPanel(mp, "Details") | |
| self.root_seed = None | |
| webchecker.Checker.__init__(self) | |
| if root: | |
| root = str(root).strip() | |
| if root: | |
| self.suggestroot(root) | |
| self.newstatus() | |
| def reset(self): | |
| webchecker.Checker.reset(self) | |
| for p in self.__todo, self.__done, self.__bad, self.__errors: | |
| p.clear() | |
| if self.root_seed: | |
| self.suggestroot(self.root_seed) | |
| def suggestroot(self, root): | |
| self.__rootentry.delete(0, END) | |
| self.__rootentry.insert(END, root) | |
| self.__rootentry.select_range(0, END) | |
| self.root_seed = root | |
| def enterroot(self, event=None): | |
| root = self.__rootentry.get() | |
| root = root.strip() | |
| if root: | |
| self.__checking.config(text="Adding root "+root) | |
| self.__checking.update_idletasks() | |
| self.addroot(root) | |
| self.__checking.config(text="Idle") | |
| try: | |
| i = self.__todo.items.index(root) | |
| except (ValueError, IndexError): | |
| pass | |
| else: | |
| self.__todo.list.select_clear(0, END) | |
| self.__todo.list.select_set(i) | |
| self.__todo.list.yview(i) | |
| self.__rootentry.delete(0, END) | |
| def start(self): | |
| self.__start.config(state=DISABLED, relief=SUNKEN) | |
| self.__stop.config(state=NORMAL) | |
| self.__step.config(state=DISABLED) | |
| self.enterroot() | |
| self.__running = 1 | |
| self.go() | |
| def stop(self): | |
| self.__stop.config(state=DISABLED, relief=SUNKEN) | |
| self.__running = 0 | |
| def step(self): | |
| self.__start.config(state=DISABLED) | |
| self.__step.config(state=DISABLED, relief=SUNKEN) | |
| self.enterroot() | |
| self.__running = 0 | |
| self.dosomething() | |
| def go(self): | |
| if self.__running: | |
| self.__parent.after_idle(self.dosomething) | |
| else: | |
| self.__checking.config(text="Idle") | |
| self.__start.config(state=NORMAL, relief=RAISED) | |
| self.__stop.config(state=DISABLED, relief=RAISED) | |
| self.__step.config(state=NORMAL, relief=RAISED) | |
| __busy = 0 | |
| def dosomething(self): | |
| if self.__busy: return | |
| self.__busy = 1 | |
| if self.todo: | |
| l = self.__todo.selectedindices() | |
| if l: | |
| i = l[0] | |
| else: | |
| i = 0 | |
| self.__todo.list.select_set(i) | |
| self.__todo.list.yview(i) | |
| url = self.__todo.items[i] | |
| self.__checking.config(text="Checking "+self.format_url(url)) | |
| self.__parent.update() | |
| self.dopage(url) | |
| else: | |
| self.stop() | |
| self.__busy = 0 | |
| self.go() | |
| def showinfo(self, url): | |
| d = self.__details | |
| d.clear() | |
| d.put("URL: %s\n" % self.format_url(url)) | |
| if self.bad.has_key(url): | |
| d.put("Error: %s\n" % str(self.bad[url])) | |
| if url in self.roots: | |
| d.put("Note: This is a root URL\n") | |
| if self.done.has_key(url): | |
| d.put("Status: checked\n") | |
| o = self.done[url] | |
| elif self.todo.has_key(url): | |
| d.put("Status: to check\n") | |
| o = self.todo[url] | |
| else: | |
| d.put("Status: unknown (!)\n") | |
| o = [] | |
| if (not url[1]) and self.errors.has_key(url[0]): | |
| d.put("Bad links from this page:\n") | |
| for triple in self.errors[url[0]]: | |
| link, rawlink, msg = triple | |
| d.put(" HREF %s" % self.format_url(link)) | |
| if self.format_url(link) != rawlink: d.put(" (%s)" %rawlink) | |
| d.put("\n") | |
| d.put(" error %s\n" % str(msg)) | |
| self.__mp.showpanel("Details") | |
| for source, rawlink in o: | |
| d.put("Origin: %s" % source) | |
| if rawlink != self.format_url(url): | |
| d.put(" (%s)" % rawlink) | |
| d.put("\n") | |
| d.text.yview("1.0") | |
| def setbad(self, url, msg): | |
| webchecker.Checker.setbad(self, url, msg) | |
| self.__bad.insert(url) | |
| self.newstatus() | |
| def setgood(self, url): | |
| webchecker.Checker.setgood(self, url) | |
| self.__bad.remove(url) | |
| self.newstatus() | |
| def newlink(self, url, origin): | |
| webchecker.Checker.newlink(self, url, origin) | |
| if self.done.has_key(url): | |
| self.__done.insert(url) | |
| elif self.todo.has_key(url): | |
| self.__todo.insert(url) | |
| self.newstatus() | |
| def markdone(self, url): | |
| webchecker.Checker.markdone(self, url) | |
| self.__done.insert(url) | |
| self.__todo.remove(url) | |
| self.newstatus() | |
| def seterror(self, url, triple): | |
| webchecker.Checker.seterror(self, url, triple) | |
| self.__errors.insert((url, '')) | |
| self.newstatus() | |
| def newstatus(self): | |
| self.__status.config(text="Status: "+self.status()) | |
| self.__parent.update() | |
| def update_checkext(self): | |
| self.checkext = self.__cv.get() | |
| class ListPanel: | |
| def __init__(self, mp, name, checker, showinfo=None): | |
| self.mp = mp | |
| self.name = name | |
| self.showinfo = showinfo | |
| self.checker = checker | |
| self.panel = mp.addpanel(name) | |
| self.list, self.frame = tktools.make_list_box( | |
| self.panel, width=60, height=5) | |
| self.list.config(exportselection=0) | |
| if showinfo: | |
| self.list.bind('<Double-Button-1>', self.doubleclick) | |
| self.items = [] | |
| def clear(self): | |
| self.items = [] | |
| self.list.delete(0, END) | |
| self.mp.hidepanel(self.name) | |
| def doubleclick(self, event): | |
| l = self.selectedindices() | |
| if l: | |
| self.showinfo(self.items[l[0]]) | |
| def selectedindices(self): | |
| l = self.list.curselection() | |
| if not l: return [] | |
| return map(int, l) | |
| def insert(self, url): | |
| if url not in self.items: | |
| if not self.items: | |
| self.mp.showpanel(self.name) | |
| # (I tried sorting alphabetically, but the display is too jumpy) | |
| i = len(self.items) | |
| self.list.insert(i, self.checker.format_url(url)) | |
| self.list.yview(i) | |
| self.items.insert(i, url) | |
| def remove(self, url): | |
| try: | |
| i = self.items.index(url) | |
| except (ValueError, IndexError): | |
| pass | |
| else: | |
| was_selected = i in self.selectedindices() | |
| self.list.delete(i) | |
| del self.items[i] | |
| if not self.items: | |
| self.mp.hidepanel(self.name) | |
| elif was_selected: | |
| if i >= len(self.items): | |
| i = len(self.items) - 1 | |
| self.list.select_set(i) | |
| class LogPanel: | |
| def __init__(self, mp, name): | |
| self.mp = mp | |
| self.name = name | |
| self.panel = mp.addpanel(name) | |
| self.text, self.frame = tktools.make_text_box(self.panel, height=10) | |
| self.text.config(wrap=NONE) | |
| def clear(self): | |
| self.text.delete("1.0", END) | |
| self.text.yview("1.0") | |
| def put(self, s): | |
| self.text.insert(END, s) | |
| if '\n' in s: | |
| self.text.yview(END) | |
| def write(self, s): | |
| self.text.insert(END, s) | |
| if '\n' in s: | |
| self.text.yview(END) | |
| self.panel.update() | |
| class MultiPanel: | |
| def __init__(self, parent): | |
| self.parent = parent | |
| self.frame = Frame(self.parent) | |
| self.frame.pack(expand=1, fill=BOTH) | |
| self.topframe = Frame(self.frame, borderwidth=2, relief=RAISED) | |
| self.topframe.pack(fill=X) | |
| self.botframe = Frame(self.frame) | |
| self.botframe.pack(expand=1, fill=BOTH) | |
| self.panelnames = [] | |
| self.panels = {} | |
| def addpanel(self, name, on=0): | |
| v = StringVar(self.parent) | |
| if on: | |
| v.set(name) | |
| else: | |
| v.set("") | |
| check = Checkbutton(self.topframe, text=name, | |
| offvalue="", onvalue=name, variable=v, | |
| command=self.checkpanel) | |
| check.pack(side=LEFT) | |
| panel = Frame(self.botframe) | |
| label = Label(panel, text=name, borderwidth=2, relief=RAISED, anchor=W) | |
| label.pack(side=TOP, fill=X) | |
| t = v, check, panel | |
| self.panelnames.append(name) | |
| self.panels[name] = t | |
| if on: | |
| panel.pack(expand=1, fill=BOTH) | |
| return panel | |
| def showpanel(self, name): | |
| v, check, panel = self.panels[name] | |
| v.set(name) | |
| panel.pack(expand=1, fill=BOTH) | |
| def hidepanel(self, name): | |
| v, check, panel = self.panels[name] | |
| v.set("") | |
| panel.pack_forget() | |
| def checkpanel(self): | |
| for name in self.panelnames: | |
| v, check, panel = self.panels[name] | |
| panel.pack_forget() | |
| for name in self.panelnames: | |
| v, check, panel = self.panels[name] | |
| if v.get(): | |
| panel.pack(expand=1, fill=BOTH) | |
| if __name__ == '__main__': | |
| main() |