Updated for 2.1a3
[python/dscho.git] / Tools / webchecker / wcgui.py
blobae012bfa8a7724eec8c49884f1682afc753a2f75
1 #! /usr/bin/env python
3 """GUI interface to webchecker.
5 This works as a Grail applet too! E.g.
7 <APPLET CODE=wcgui.py NAME=CheckerWindow></APPLET>
9 Checkpoints are not (yet??? ever???) supported.
11 User interface:
13 Enter a root to check in the text entry box. To enter more than one root,
14 enter them one at a time and press <Return> for each one.
16 Command buttons Start, Stop and "Check one" govern the checking process in
17 the obvious way. Start and "Check one" also enter the root from the text
18 entry box if one is present. There's also a check box (enabled by default)
19 to decide whether actually to follow external links (since this can slow
20 the checking down considerably). Finally there's a Quit button.
22 A series of checkbuttons determines whether the corresponding output panel
23 is shown. List panels are also automatically shown or hidden when their
24 status changes between empty to non-empty. There are six panels:
26 Log -- raw output from the checker (-v, -q affect this)
27 To check -- links discovered but not yet checked
28 Checked -- links that have been checked
29 Bad links -- links that failed upon checking
30 Errors -- pages containing at least one bad link
31 Details -- details about one URL; double click on a URL in any of
32 the above list panels (not in Log) will show details
33 for that URL
35 Use your window manager's Close command to quit.
37 Command line options:
39 -m bytes -- skip HTML pages larger than this size (default %(MAXPAGE)d)
40 -q -- quiet operation (also suppresses external links report)
41 -v -- verbose operation; repeating -v will increase verbosity
42 -t root -- specify root dir which should be treated as internal (can repeat)
43 -a -- don't check name anchors
45 Command line arguments:
47 rooturl -- URL to start checking
48 (default %(DEFROOT)s)
50 XXX The command line options (-m, -q, -v) should be GUI accessible.
52 XXX The roots should be visible as a list (?).
54 XXX The multipanel user interface is clumsy.
56 """
58 # ' Emacs bait
61 import sys
62 import getopt
63 import string
64 from Tkinter import *
65 import tktools
66 import webchecker
67 import random
69 # Override some for a weaker platform
70 if sys.platform == 'mac':
71 webchecker.DEFROOT = "http://grail.cnri.reston.va.us/"
72 webchecker.MAXPAGE = 50000
73 webchecker.verbose = 4
75 def main():
76 try:
77 opts, args = getopt.getopt(sys.argv[1:], 't:m:qva')
78 except getopt.error, msg:
79 sys.stdout = sys.stderr
80 print msg
81 print __doc__%vars(webchecker)
82 sys.exit(2)
83 webchecker.verbose = webchecker.VERBOSE
84 webchecker.nonames = webchecker.NONAMES
85 webchecker.maxpage = webchecker.MAXPAGE
86 extra_roots = []
87 for o, a in opts:
88 if o == '-m':
89 webchecker.maxpage = string.atoi(a)
90 if o == '-q':
91 webchecker.verbose = 0
92 if o == '-v':
93 webchecker.verbose = webchecker.verbose + 1
94 if o == '-t':
95 extra_roots.append(a)
96 if o == '-a':
97 webchecker.nonames = not webchecker.nonames
98 root = Tk(className='Webchecker')
99 root.protocol("WM_DELETE_WINDOW", root.quit)
100 c = CheckerWindow(root)
101 c.setflags(verbose=webchecker.verbose, maxpage=webchecker.maxpage,
102 nonames=webchecker.nonames)
103 if args:
104 for arg in args[:-1]:
105 c.addroot(arg)
106 c.suggestroot(args[-1])
107 # Usually conditioned on whether external links
108 # will be checked, but since that's not a command
109 # line option, just toss them in.
110 for url_root in extra_roots:
111 # Make sure it's terminated by a slash,
112 # so that addroot doesn't discard the last
113 # directory component.
114 if url_root[-1] != "/":
115 url_root = url_root + "/"
116 c.addroot(url_root, add_to_do = 0)
117 root.mainloop()
120 class CheckerWindow(webchecker.Checker):
122 def __init__(self, parent, root=webchecker.DEFROOT):
123 self.__parent = parent
125 self.__topcontrols = Frame(parent)
126 self.__topcontrols.pack(side=TOP, fill=X)
127 self.__label = Label(self.__topcontrols, text="Root URL:")
128 self.__label.pack(side=LEFT)
129 self.__rootentry = Entry(self.__topcontrols, width=60)
130 self.__rootentry.pack(side=LEFT)
131 self.__rootentry.bind('<Return>', self.enterroot)
132 self.__rootentry.focus_set()
134 self.__controls = Frame(parent)
135 self.__controls.pack(side=TOP, fill=X)
136 self.__running = 0
137 self.__start = Button(self.__controls, text="Run", command=self.start)
138 self.__start.pack(side=LEFT)
139 self.__stop = Button(self.__controls, text="Stop", command=self.stop,
140 state=DISABLED)
141 self.__stop.pack(side=LEFT)
142 self.__step = Button(self.__controls, text="Check one",
143 command=self.step)
144 self.__step.pack(side=LEFT)
145 self.__cv = BooleanVar(parent)
146 self.__cv.set(self.checkext)
147 self.__checkext = Checkbutton(self.__controls, variable=self.__cv,
148 command=self.update_checkext,
149 text="Check nonlocal links",)
150 self.__checkext.pack(side=LEFT)
151 self.__reset = Button(self.__controls, text="Start over", command=self.reset)
152 self.__reset.pack(side=LEFT)
153 if __name__ == '__main__': # No Quit button under Grail!
154 self.__quit = Button(self.__controls, text="Quit",
155 command=self.__parent.quit)
156 self.__quit.pack(side=RIGHT)
158 self.__status = Label(parent, text="Status: initial", anchor=W)
159 self.__status.pack(side=TOP, fill=X)
160 self.__checking = Label(parent, text="Idle", anchor=W)
161 self.__checking.pack(side=TOP, fill=X)
162 self.__mp = mp = MultiPanel(parent)
163 sys.stdout = self.__log = LogPanel(mp, "Log")
164 self.__todo = ListPanel(mp, "To check", self, self.showinfo)
165 self.__done = ListPanel(mp, "Checked", self, self.showinfo)
166 self.__bad = ListPanel(mp, "Bad links", self, self.showinfo)
167 self.__errors = ListPanel(mp, "Pages w/ bad links", self, self.showinfo)
168 self.__details = LogPanel(mp, "Details")
169 self.root_seed = None
170 webchecker.Checker.__init__(self)
171 if root:
172 root = string.strip(str(root))
173 if root:
174 self.suggestroot(root)
175 self.newstatus()
177 def reset(self):
178 webchecker.Checker.reset(self)
179 for p in self.__todo, self.__done, self.__bad, self.__errors:
180 p.clear()
181 if self.root_seed:
182 self.suggestroot(self.root_seed)
184 def suggestroot(self, root):
185 self.__rootentry.delete(0, END)
186 self.__rootentry.insert(END, root)
187 self.__rootentry.select_range(0, END)
188 self.root_seed = root
190 def enterroot(self, event=None):
191 root = self.__rootentry.get()
192 root = string.strip(root)
193 if root:
194 self.__checking.config(text="Adding root "+root)
195 self.__checking.update_idletasks()
196 self.addroot(root)
197 self.__checking.config(text="Idle")
198 try:
199 i = self.__todo.items.index(root)
200 except (ValueError, IndexError):
201 pass
202 else:
203 self.__todo.list.select_clear(0, END)
204 self.__todo.list.select_set(i)
205 self.__todo.list.yview(i)
206 self.__rootentry.delete(0, END)
208 def start(self):
209 self.__start.config(state=DISABLED, relief=SUNKEN)
210 self.__stop.config(state=NORMAL)
211 self.__step.config(state=DISABLED)
212 self.enterroot()
213 self.__running = 1
214 self.go()
216 def stop(self):
217 self.__stop.config(state=DISABLED, relief=SUNKEN)
218 self.__running = 0
220 def step(self):
221 self.__start.config(state=DISABLED)
222 self.__step.config(state=DISABLED, relief=SUNKEN)
223 self.enterroot()
224 self.__running = 0
225 self.dosomething()
227 def go(self):
228 if self.__running:
229 self.__parent.after_idle(self.dosomething)
230 else:
231 self.__checking.config(text="Idle")
232 self.__start.config(state=NORMAL, relief=RAISED)
233 self.__stop.config(state=DISABLED, relief=RAISED)
234 self.__step.config(state=NORMAL, relief=RAISED)
236 __busy = 0
238 def dosomething(self):
239 if self.__busy: return
240 self.__busy = 1
241 if self.todo:
242 l = self.__todo.selectedindices()
243 if l:
244 i = l[0]
245 else:
246 i = 0
247 self.__todo.list.select_set(i)
248 self.__todo.list.yview(i)
249 url = self.__todo.items[i]
250 self.__checking.config(text="Checking "+self.format_url(url))
251 self.__parent.update()
252 self.dopage(url)
253 else:
254 self.stop()
255 self.__busy = 0
256 self.go()
258 def showinfo(self, url):
259 d = self.__details
260 d.clear()
261 d.put("URL: %s\n" % self.format_url(url))
262 if self.bad.has_key(url):
263 d.put("Error: %s\n" % str(self.bad[url]))
264 if url in self.roots:
265 d.put("Note: This is a root URL\n")
266 if self.done.has_key(url):
267 d.put("Status: checked\n")
268 o = self.done[url]
269 elif self.todo.has_key(url):
270 d.put("Status: to check\n")
271 o = self.todo[url]
272 else:
273 d.put("Status: unknown (!)\n")
274 o = []
275 if (not url[1]) and self.errors.has_key(url[0]):
276 d.put("Bad links from this page:\n")
277 for triple in self.errors[url[0]]:
278 link, rawlink, msg = triple
279 d.put(" HREF %s" % self.format_url(link))
280 if self.format_url(link) != rawlink: d.put(" (%s)" %rawlink)
281 d.put("\n")
282 d.put(" error %s\n" % str(msg))
283 self.__mp.showpanel("Details")
284 for source, rawlink in o:
285 d.put("Origin: %s" % source)
286 if rawlink != self.format_url(url):
287 d.put(" (%s)" % rawlink)
288 d.put("\n")
289 d.text.yview("1.0")
291 def setbad(self, url, msg):
292 webchecker.Checker.setbad(self, url, msg)
293 self.__bad.insert(url)
294 self.newstatus()
296 def setgood(self, url):
297 webchecker.Checker.setgood(self, url)
298 self.__bad.remove(url)
299 self.newstatus()
301 def newlink(self, url, origin):
302 webchecker.Checker.newlink(self, url, origin)
303 if self.done.has_key(url):
304 self.__done.insert(url)
305 elif self.todo.has_key(url):
306 self.__todo.insert(url)
307 self.newstatus()
309 def markdone(self, url):
310 webchecker.Checker.markdone(self, url)
311 self.__done.insert(url)
312 self.__todo.remove(url)
313 self.newstatus()
315 def seterror(self, url, triple):
316 webchecker.Checker.seterror(self, url, triple)
317 self.__errors.insert((url, ''))
318 self.newstatus()
320 def newstatus(self):
321 self.__status.config(text="Status: "+self.status())
322 self.__parent.update()
324 def update_checkext(self):
325 self.checkext = self.__cv.get()
328 class ListPanel:
330 def __init__(self, mp, name, checker, showinfo=None):
331 self.mp = mp
332 self.name = name
333 self.showinfo = showinfo
334 self.checker = checker
335 self.panel = mp.addpanel(name)
336 self.list, self.frame = tktools.make_list_box(
337 self.panel, width=60, height=5)
338 self.list.config(exportselection=0)
339 if showinfo:
340 self.list.bind('<Double-Button-1>', self.doubleclick)
341 self.items = []
343 def clear(self):
344 self.items = []
345 self.list.delete(0, END)
346 self.mp.hidepanel(self.name)
348 def doubleclick(self, event):
349 l = self.selectedindices()
350 if l:
351 self.showinfo(self.items[l[0]])
353 def selectedindices(self):
354 l = self.list.curselection()
355 if not l: return []
356 return map(string.atoi, l)
358 def insert(self, url):
359 if url not in self.items:
360 if not self.items:
361 self.mp.showpanel(self.name)
362 # (I tried sorting alphabetically, but the display is too jumpy)
363 i = len(self.items)
364 self.list.insert(i, self.checker.format_url(url))
365 self.list.yview(i)
366 self.items.insert(i, url)
368 def remove(self, url):
369 try:
370 i = self.items.index(url)
371 except (ValueError, IndexError):
372 pass
373 else:
374 was_selected = i in self.selectedindices()
375 self.list.delete(i)
376 del self.items[i]
377 if not self.items:
378 self.mp.hidepanel(self.name)
379 elif was_selected:
380 if i >= len(self.items):
381 i = len(self.items) - 1
382 self.list.select_set(i)
385 class LogPanel:
387 def __init__(self, mp, name):
388 self.mp = mp
389 self.name = name
390 self.panel = mp.addpanel(name)
391 self.text, self.frame = tktools.make_text_box(self.panel, height=10)
392 self.text.config(wrap=NONE)
394 def clear(self):
395 self.text.delete("1.0", END)
396 self.text.yview("1.0")
398 def put(self, s):
399 self.text.insert(END, s)
400 if '\n' in s:
401 self.text.yview(END)
403 def write(self, s):
404 self.text.insert(END, s)
405 if '\n' in s:
406 self.text.yview(END)
407 self.panel.update()
410 class MultiPanel:
412 def __init__(self, parent):
413 self.parent = parent
414 self.frame = Frame(self.parent)
415 self.frame.pack(expand=1, fill=BOTH)
416 self.topframe = Frame(self.frame, borderwidth=2, relief=RAISED)
417 self.topframe.pack(fill=X)
418 self.botframe = Frame(self.frame)
419 self.botframe.pack(expand=1, fill=BOTH)
420 self.panelnames = []
421 self.panels = {}
423 def addpanel(self, name, on=0):
424 v = StringVar(self.parent)
425 if on:
426 v.set(name)
427 else:
428 v.set("")
429 check = Checkbutton(self.topframe, text=name,
430 offvalue="", onvalue=name, variable=v,
431 command=self.checkpanel)
432 check.pack(side=LEFT)
433 panel = Frame(self.botframe)
434 label = Label(panel, text=name, borderwidth=2, relief=RAISED, anchor=W)
435 label.pack(side=TOP, fill=X)
436 t = v, check, panel
437 self.panelnames.append(name)
438 self.panels[name] = t
439 if on:
440 panel.pack(expand=1, fill=BOTH)
441 return panel
443 def showpanel(self, name):
444 v, check, panel = self.panels[name]
445 v.set(name)
446 panel.pack(expand=1, fill=BOTH)
448 def hidepanel(self, name):
449 v, check, panel = self.panels[name]
450 v.set("")
451 panel.pack_forget()
453 def checkpanel(self):
454 for name in self.panelnames:
455 v, check, panel = self.panels[name]
456 panel.pack_forget()
457 for name in self.panelnames:
458 v, check, panel = self.panels[name]
459 if v.get():
460 panel.pack(expand=1, fill=BOTH)
463 if __name__ == '__main__':
464 main()