blob: bc02accd4794dd3519346bf1c8b825e7f69b34c0 [file] [log] [blame]
Guido van Rossumadb3a9d1997-05-21 07:24:50 +00001#! /depot/sundry/plat/bin/python1.4
2
Guido van Rossumd7bfa801997-05-21 21:31:39 +00003"""Interactive FAQ project.
4
5XXX TO DO
6
7- use cookies to keep Name/email the same
8- explanation of editing somewhere
9- various embellishments, GIFs, crosslinks, hints, etc.
10- create new sections
11- rearrange entries
12- delete entries
13- log changes
14- send email on changes
15- optional staging of entries until reviewed?
16- review revision log and older versions
17- freeze entries
18- username/password for editors
19- Change references to other Q's and whole sections
20- Browse should display menu of 7 sections & let you pick
21 (or frontpage should have the option to browse a section or all)
22- support adding annotations, too
23
24"""
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000025
26import cgi, string, os
27
28NAMEPAT = "faq??.???.htp"
Guido van Rossumd7bfa801997-05-21 21:31:39 +000029NAMEREG = "^faq\([0-9][0-9]\)\.\([0-9][0-9][0-9]\)\.htp$"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000030
31class FAQServer:
32
33 def __init__(self):
34 pass
35
36 def main(self):
37 self.form = cgi.FieldStorage()
38 req = self.req or 'frontpage'
39 try:
40 method = getattr(self, 'do_%s' % req)
41 except AttributeError:
42 print "Unrecognized request type", req
43 else:
44 method()
45
Guido van Rossum3c3354c1997-05-21 16:52:18 +000046 KEYS = ['req', 'query', 'name', 'text', 'commit', 'title',
Guido van Rossumd7bfa801997-05-21 21:31:39 +000047 'author', 'email', 'log', 'section', 'number', 'add']
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000048
49 def __getattr__(self, key):
50 if key not in self.KEYS:
51 raise AttributeError
Guido van Rossum3c3354c1997-05-21 16:52:18 +000052 try:
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000053 item = self.form[key]
54 return item.value
Guido van Rossum3c3354c1997-05-21 16:52:18 +000055 except KeyError:
56 return ''
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000057
58 def do_frontpage(self):
59 print """
60 <TITLE>Python FAQ (alpha 1)</TITLE>
61 <H1>Python FAQ Front Page</H1>
62 <UL>
Guido van Rossumd7bfa801997-05-21 21:31:39 +000063 <LI><A HREF="faq.py?req=index">FAQ index</A>
64 <LI><A HREF="faq.py?req=all">The whole FAQ</A>
Guido van Rossum3c3354c1997-05-21 16:52:18 +000065 <LI><A HREF="faq.py?req=roulette">FAQ roulette</A>
Guido van Rossumd7bfa801997-05-21 21:31:39 +000066 <LI><A HREF="faq.py?req=recent">Recently changed FAQ entries</A>
67 <LI><A HREF="faq.py?req=add">Add a new FAQ entry</A>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000068 </UL>
69
Guido van Rossumd7bfa801997-05-21 21:31:39 +000070 <H2>Search the FAQ</H2>
71
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000072 <FORM ACTION="faq.py?req=query">
73 <INPUT TYPE=text NAME=query>
74 <INPUT TYPE=submit VALUE="Search">
75 <INPUT TYPE=hidden NAME=req VALUE=query>
76 </FORM>
77
78 Disclaimer: these pages are intended to be edited by anyone.
79 Please exercise discretion when editing, don't be rude, etc.
80 """
81
Guido van Rossumd7bfa801997-05-21 21:31:39 +000082 def do_index(self):
83 print """
84 <TITLE>Python FAQ Index</TITLE>
85 <H1>Python FAQ Index</H1>
86 """
87 names = os.listdir(os.curdir)
88 names.sort()
89 section = None
90 for name in names:
91 headers, text = self.read(name)
92 if headers:
93 title = headers['title']
94 i = string.find(title, '.')
95 nsec = title[:i]
96 if nsec != section:
97 if section:
98 print """
99 <P>
100 <LI><A HREF="faq.py?req=add&amp;section=%s"
101 >Add new entry</A> (at this point)
102 </UL>
103 """ % section
104 section = nsec
105 print "<H2>Section %s</H2>" % section
106 print "<UL>"
107 print '<LI><A HREF="faq.py?req=show&name=%s">%s</A>' % (
108 name, cgi.escape(title))
109 if section:
110 print """
111 <P>
112 <LI><A HREF="faq.py?req=add&amp;section=%s">Add new entry</A>
113 (at this point)
114 </UL>
115 """ % section
116 else:
117 print "No FAQ entries?!?!"
118
119 def do_show(self):
120 name = self.name
121 headers, text = self.read(name)
122 if not headers:
123 print "Invalid file name", name
124 return
125 self.show(name, headers['title'], text, 1)
126
127 def do_all(self):
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000128 print """
129 <TITLE>Python FAQ</TITLE>
130 <H1>Python FAQ</H1>
131 <HR>
132 """
133 names = os.listdir(os.curdir)
134 names.sort()
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000135 section = None
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000136 for name in names:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000137 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000138 if headers:
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000139 title = headers['title']
140 i = string.find(title, '.')
141 nsec = title[:i]
142 if nsec != section:
143 section = nsec
144 print "<H1>Section %s</H1>" % section
145 print "<HR>"
146 self.show(name, title, text, 1)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000147 n = n+1
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000148 if not section:
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000149 print "No FAQ entries?!?!"
150
151 def do_roulette(self):
152 import whrandom
153 print """
154 <TITLE>Python FAQ Roulette</TITLE>
155 <H1>Python FAQ Roulette</H1>
156 Please check the correctness of the entry below.
157 If you find any problems, please edit the entry.
158 <P>
159 <HR>
160 """
161 names = os.listdir(os.curdir)
162 while names:
163 name = whrandom.choice(names)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000164 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000165 if headers:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000166 self.show(name, headers['title'], text, 1)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000167 print '<P><A HREF="faq.py?req=roulette">Show another one</A>'
168 break
169 else:
170 names.remove(name)
171 else:
172 print "No FAQ entries?!?!"
173
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000174 def do_recent(self):
175 import fnmatch, stat
176 names = os.listdir(os.curdir)
177 now = time.time()
178 list = []
179 for name in names:
180 if not fnmatch.fnmatch(name, NAMEPAT):
181 continue
182 try:
183 st = os.stat(name)
184 except os.error:
185 continue
186 tuple = (st[stat.ST_MTIME], name)
187 list.append(tuple)
188 list.sort()
189 list.reverse()
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000190 print """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000191 <TITLE>Python FAQ, Most Recently Modified First</TITLE>
192 <H1>Python FAQ, Most Recently Modified First</H1>
193 <HR>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000194 """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000195 n = 0
196 for (mtime, name) in list:
197 headers, text = self.read(name)
198 if headers:
199 self.show(name, headers['title'], text, 1)
200 n = n+1
201 if not n:
202 print "No FAQ entries?!?!"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000203
204 def do_query(self):
205 import regex
206 print "<TITLE>Python FAQ Query Results</TITLE>"
207 print "<H1>Python FAQ Query Results</H1>"
208 query = self.query
209 if not query:
210 print "No query string"
211 return
212 p = regex.compile(query, regex.casefold)
213 names = os.listdir(os.curdir)
214 names.sort()
215 print "<HR>"
216 n = 0
217 for name in names:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000218 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000219 if headers:
220 title = headers['title']
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000221 if p.search(title) >= 0 or p.search(text) >= 0:
222 self.show(name, title, text, 1)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000223 n = n+1
224 if not n:
225 print "No hits."
226
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000227 def do_add(self):
228 section = self.section
229 if not section:
230 print """
231 <TITLE>How to add a new FAQ entry</TITLE>
232 <H1>How to add a new FAQ entry</H1>
233
234 Go to the <A HREF="faq.py?req=index">FAQ index</A>
235 and click on the "Add new entry" link at the end
236 of the section to which you want to add the entry.
237 """
238 return
239 try:
240 nsec = string.atoi(section)
241 except ValueError:
242 print "Bad section number", nsec
243 names = os.listdir(os.curdir)
244 max = 0
245 import regex
246 prog = regex.compile(NAMEREG)
247 for name in names:
248 if prog.match(name) >= 0:
249 s1, s2 = prog.group(1, 2)
250 n1, n2 = string.atoi(s1), string.atoi(s2)
251 if n1 == nsec:
252 if n2 > max:
253 max = n2
254 if not max:
255 print "Can't add new sections yet."
256 return
257 num = max+1
258 name = "faq%02d.%03d.htp" % (nsec, num)
259 self.name = name
260 self.add = "yes"
261 self.number = str(num)
262 self.do_edit()
263
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000264 def do_edit(self):
265 name = self.name
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000266 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000267 if not headers:
268 print "Invalid file name", name
269 return
270 print """
271 <TITLE>Python FAQ Edit Form</TITLE>
272 <H1>Python FAQ Edit Form</H1>
273 """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000274 title = headers['title']
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000275 print "<FORM METHOD=POST ACTION=faq.py>"
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000276 self.showedit(name, title, text)
277 if self.add:
278 print """
279 <INPUT TYPE=hidden NAME=add VALUE=%s>
280 <INPUT TYPE=hidden NAME=section VALUE=%s>
281 <INPUT TYPE=hidden NAME=number VALUE=%s>
282 """ % (self.add, self.section, self.number)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000283 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000284 <INPUT TYPE=submit VALUE="Review Edit">
285 <INPUT TYPE=hidden NAME=req VALUE=review>
286 <INPUT TYPE=hidden NAME=name VALUE=%s>
287 </FORM>
288 <HR>
289 """ % name
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000290 self.show(name, title, text)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000291
292 def do_review(self):
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000293 if self.commit:
294 self.checkin()
295 return
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000296 name = self.name
297 text = self.text
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000298 title = self.title
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000299 headers, oldtext = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000300 if not headers:
301 print "Invalid file name", name
302 return
303 print """
304 <TITLE>Python FAQ Review Form</TITLE>
305 <H1>Python FAQ Review Form</H1>
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000306 <HR>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000307 """
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000308 self.show(name, title, text)
309 print "<FORM METHOD=POST ACTION=faq.py>"
310 if self.log and self.author and '@' in self.email:
311 print """
312 <INPUT TYPE=submit NAME=commit VALUE="Commit">
313 Click this button to commit the change.
314 <P>
315 <HR>
316 <P>
317 """
318 else:
319 print """
320 To commit this change, please enter your name,
321 email and a log message in the form below.
322 <P>
323 <HR>
324 <P>
325 """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000326 self.showedit(name, title, text)
327 if self.add:
328 print """
329 <INPUT TYPE=hidden NAME=add VALUE=%s>
330 <INPUT TYPE=hidden NAME=section VALUE=%s>
331 <INPUT TYPE=hidden NAME=number VALUE=%s>
332 """ % (self.add, self.section, self.number)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000333 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000334 <BR>
335 <INPUT TYPE=submit VALUE="Review Edit">
336 <INPUT TYPE=hidden NAME=req VALUE=review>
337 <INPUT TYPE=hidden NAME=name VALUE=%s>
338 </FORM>
339 <HR>
340 """ % name
341
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000342 def checkin(self):
343 import regsub, time, tempfile
344 name = self.name
345
346 headers, oldtext = self.read(name)
347 if not headers:
348 print "Invalid file name", name
349 return
350 text = self.text
351 title = self.title
352 author = self.author
353 email = self.email
354 log = self.log
355 text = regsub.gsub("\r\n", "\n", text)
356 log = regsub.gsub("\r\n", "\n", log)
357 author = string.join(string.split(author))
358 email = string.join(string.split(email))
359 title = string.join(string.split(title))
360 oldtitle = headers['title']
361 oldtitle = string.join(string.split(oldtitle))
362 text = string.strip(text)
363 oldtext = string.strip(oldtext)
364 if text == oldtext and title == oldtitle:
365 print "No changes."
366 # XXX Should exit more ceremoniously
367 return
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000368 # Check that the FAQ entry number didn't change
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000369 if string.split(title)[:1] != string.split(oldtitle)[:1]:
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000370 print "Don't change the FAQ entry number please."
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000371 # XXX Should exit more ceremoniously
372 return
373 remhost = os.environ["REMOTE_HOST"]
374 remaddr = os.environ["REMOTE_ADDR"]
375 try:
376 os.unlink(name + "~")
377 except os.error:
378 pass
379 try:
380 os.rename(name, name + "~")
381 except os.error:
382 pass
383 try:
384 os.unlink(name)
385 except os.error:
386 pass
387 try:
388 f = open(name, "w")
389 except IOError, msg:
390 print "Can't open", name, "for writing:", cgi.escape(str(msg))
391 # XXX Should exit more ceremoniously
392 return
393 now = time.ctime(time.time())
394 f.write("Title: %s\n" % title)
395 f.write("Last-Changed-Date: %s\n" % now)
396 f.write("Last-Changed-Author: %s\n" % author)
397 f.write("Last-Changed-Email: %s\n" % email)
398 f.write("Last-Changed-Remote-Host: %s\n" % remhost)
399 f.write("Last-Changed-Remote-Address: %s\n" % remaddr)
400 keys = headers.keys()
401 keys.sort()
402 keys.remove('title')
403 for key in keys:
404 if key[:13] != 'last-changed-':
405 f.write("%s: %s\n" % (string.capwords(key, '-'),
406 headers[key]))
407 f.write("\n")
408 f.write(text)
409 f.write("\n")
410 f.close()
411
412 tfn = tempfile.mktemp()
413 f = open(tfn, "w")
414 f.write("Last-Changed-Date: %s\n" % now)
415 f.write("Last-Changed-Author: %s\n" % author)
416 f.write("Last-Changed-Email: %s\n" % email)
417 f.write("Last-Changed-Remote-Host: %s\n" % remhost)
418 f.write("Last-Changed-Remote-Address: %s\n" % remaddr)
419 f.write("\n")
420 f.write(log)
421 f.write("\n")
422 f.close()
423
424 p = os.popen("""
425 /depot/gnu/plat/bin/rcs -l %s </dev/null 2>&1
426 /depot/gnu/plat/bin/ci -u %s <%s 2>&1
427 rm -f %s
428 """ % (name, name, tfn, tfn))
429 output = p.read()
430 sts = p.close()
431 if not sts:
432 print """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000433 <TITLE>Python FAQ Entry Edited</TITLE>
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000434 <H1>Python FAQ Entry Edited</H1>
435 <HR>
436 """
437 self.show(name, title, text, 1)
438 if output:
439 print "<PRE>%s</PRE>" % cgi.escape(output)
440 else:
441 print """
442 <H1>Python FAQ Entry Commit Failed</H1>
443 Exit status 0x%04x
444 """ % sts
445 if output:
446 print "<PRE>%s</PRE>" % cgi.escape(output)
447
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000448 def showedit(self, name, title, text):
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000449 print """
450 Title: <INPUT TYPE=text SIZE=70 NAME=title VALUE="%s"<BR>
451 <TEXTAREA COLS=80 ROWS=20 NAME=text>""" % title
452 print cgi.escape(string.strip(text))
453 print """</TEXTAREA>
454 <BR>
455 Please provide the following information for logging purposes:
456 <BR>
457 <CODE>Name : </CODE><INPUT TYPE=text SIZE=70 NAME=author VALUE="%s"<BR>
458 <CODE>Email: </CODE><INPUT TYPE=text SIZE=70 NAME=email VALUE="%s"<BR>
459 Log message (reason for the change):<BR>
460 <TEXTAREA COLS=80 ROWS=5 NAME=log>\n%s\n</TEXTAREA>
461 """ % (self.author, self.email, self.log)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000462
463 def showheaders(self, headers):
464 print "<UL>"
465 keys = map(string.lower, headers.keys())
466 keys.sort()
467 for key in keys:
468 print "<LI><B>%s:</B> %s" % (string.capwords(key, '-'),
469 headers[key] or '')
470 print "</UL>"
471
472 def read(self, name):
473 import fnmatch, rfc822
474 if not fnmatch.fnmatch(name, NAMEPAT):
475 return None, None
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000476 if self.add:
477 try:
478 fname = "faq%02d.%03d.htp" % (string.atoi(self.section),
479 string.atoi(self.number))
480 except ValueError:
481 return None, None
482 if fname != name:
483 return None, None
484 headers = {'title': "%s.%s. " % (self.section, self.number)}
485 text = ""
486 return headers, text
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000487 f = open(name)
488 headers = rfc822.Message(f)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000489 text = f.read()
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000490 f.close()
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000491 return headers, text
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000492
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000493 def show(self, name, title, text, edit=0):
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000494 # XXX Should put <A> tags around recognizable URLs
495 # XXX Should also turn "see section N" into hyperlinks
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000496 print "<H2>%s</H2>" % cgi.escape(title)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000497 pre = 0
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000498 for line in string.split(text, '\n'):
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000499 if not string.strip(line):
500 if pre:
501 print '</PRE>'
502 pre = 0
503 else:
504 print '<P>'
505 else:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000506 if line == string.lstrip(line): # I.e., no leading whitespace
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000507 if pre:
508 print '</PRE>'
509 pre = 0
510 else:
511 if not pre:
512 print '<PRE>'
513 pre = 1
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000514 print cgi.escape(line)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000515 if pre:
516 print '</PRE>'
517 pre = 0
518 print '<P>'
519 if edit:
520 print '<A HREF="faq.py?req=edit&name=%s">Edit this entry</A>' %name
521 print '<P>'
522 print "<HR>"
523
524
525print "Content-type: text/html\n"
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000526dt = 0
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000527try:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000528 import time
529 t1 = time.time()
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000530 x = FAQServer()
531 x.main()
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000532 t2 = time.time()
533 dt = t2-t1
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000534except:
535 print "<HR>Sorry, an error occurred"
536 cgi.print_exception()
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000537print "<P>(time = %s seconds)" % str(round(dt, 3))