blob: a8e85bc9f2a79f7df80b94b4f0415b3322384390 [file] [log] [blame]
Guido van Rossumd7bfa801997-05-21 21:31:39 +00001"""Interactive FAQ project.
2
Guido van Rossumed531fd1997-05-22 15:21:57 +00003Note that this is not an executable script; it's an importable module.
4The actual CGI script can be kept minimal; it's appended at the end of
5this file as a string constant.
6
Guido van Rossumd7bfa801997-05-21 21:31:39 +00007XXX TO DO
8
Guido van Rossum1dcc2441997-05-23 18:53:06 +00009XXX User Features TO DO
10
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000011- next/prev/index links in do_show???
Guido van Rossumd7bfa801997-05-21 21:31:39 +000012- explanation of editing somewhere
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000013- embellishments, GIFs, hints, etc.
Guido van Rossum1dcc2441997-05-23 18:53:06 +000014- support adding annotations, too
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000015- restrict recent changes to last week (or make it an option)
16- extended search capabilities
Guido van Rossum1dcc2441997-05-23 18:53:06 +000017
18XXX Management Features TO DO
19
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000020- username/password for authors
Guido van Rossumd7bfa801997-05-21 21:31:39 +000021- create new sections
22- rearrange entries
23- delete entries
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000024- freeze entries
Guido van Rossum1dcc2441997-05-23 18:53:06 +000025- send email on changes?
26- send email on ERRORS!
Guido van Rossumd7bfa801997-05-21 21:31:39 +000027- optional staging of entries until reviewed?
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000028 (could be done using rcs branches!)
29- prevent race conditions on nearly simultaneous commits
30
31XXX Performance
32
33- could cache generated HTML
34- could speed up searches with a separate index file
Guido van Rossum1dcc2441997-05-23 18:53:06 +000035
36XXX Code organization TO DO
37
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000038- read section titles from a file (could be a Python file: import faqcustom)
Guido van Rossum1dcc2441997-05-23 18:53:06 +000039- customize rcs command pathnames (and everything else)
Guido van Rossumed531fd1997-05-22 15:21:57 +000040- make it more generic (so you can create your own FAQ)
Guido van Rossumc6447521997-05-23 00:50:01 +000041- more OO structure, e.g. add a class representing one FAQ entry
Guido van Rossumd7bfa801997-05-21 21:31:39 +000042
43"""
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000044
Guido van Rossumf8f0fb71997-05-23 21:21:43 +000045# NB for timing purposes, the imports are at the end of this file
46
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000047NAMEPAT = "faq??.???.htp"
Guido van Rossumd7bfa801997-05-21 21:31:39 +000048NAMEREG = "^faq\([0-9][0-9]\)\.\([0-9][0-9][0-9]\)\.htp$"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000049
Guido van Rossum4888c7e1997-05-23 15:55:19 +000050SECTIONS = {
51 "1": "General information and availability",
52 "2": "Python in the real world",
53 "3": "Building Python and Other Known Bugs",
54 "4": "Programming in Python",
55 "5": "Extending Python",
56 "6": "Python's design",
57 "7": "Using Python on non-UNIX platforms",
58}
59
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000060class FAQServer:
61
62 def __init__(self):
63 pass
64
65 def main(self):
66 self.form = cgi.FieldStorage()
67 req = self.req or 'frontpage'
68 try:
69 method = getattr(self, 'do_%s' % req)
70 except AttributeError:
71 print "Unrecognized request type", req
72 else:
73 method()
Guido van Rossum74427e51997-05-21 23:43:39 +000074 self.epilogue()
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000075
Guido van Rossum3c3354c1997-05-21 16:52:18 +000076 KEYS = ['req', 'query', 'name', 'text', 'commit', 'title',
Guido van Rossum74427e51997-05-21 23:43:39 +000077 'author', 'email', 'log', 'section', 'number', 'add',
Guido van Rossum4888c7e1997-05-23 15:55:19 +000078 'version', 'edit']
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000079
80 def __getattr__(self, key):
81 if key not in self.KEYS:
82 raise AttributeError
Guido van Rossum3c3354c1997-05-21 16:52:18 +000083 try:
Guido van Rossumed531fd1997-05-22 15:21:57 +000084 form = self.form
85 try:
86 item = form[key]
87 except TypeError, msg:
88 raise KeyError, msg, sys.exc_traceback
Guido van Rossum3c3354c1997-05-21 16:52:18 +000089 except KeyError:
90 return ''
Guido van Rossumaf5be951997-05-22 16:57:50 +000091 value = self.form[key].value
92 value = string.strip(value)
93 setattr(self, key, value)
94 return value
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000095
96 def do_frontpage(self):
Guido van Rossum74427e51997-05-21 23:43:39 +000097 self.prologue("Python FAQ (alpha) Front Page")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000098 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000099 <UL>
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000100 <LI><A HREF="faq.py?req=index">FAQ index</A>
101 <LI><A HREF="faq.py?req=all">The whole FAQ</A>
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000102 <LI><A HREF="faq.py?req=roulette">FAQ roulette</A>
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000103 <LI><A HREF="faq.py?req=recent">Recently changed FAQ entries</A>
104 <LI><A HREF="faq.py?req=add">Add a new FAQ entry</A>
Guido van Rossumaf5be951997-05-22 16:57:50 +0000105 <LI><A HREF="faq.py?req=delete">Delete a FAQ entry</A>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000106 </UL>
107
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000108 <HR>
109
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000110 <H2>Search the FAQ</H2>
111
Guido van Rossuma0e9a6d1997-05-23 18:13:58 +0000112 <FORM ACTION="faq.py">
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000113 <INPUT TYPE=text NAME=query>
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000114 <INPUT TYPE=submit VALUE="Search"><BR>
115 (Case insensitive regular expressions.)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000116 <INPUT TYPE=hidden NAME=req VALUE=query>
117 </FORM>
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000118 <HR>
119 <P>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000120 Disclaimer: these pages are intended to be edited by anyone.
121 Please exercise discretion when editing, don't be rude, etc.
122 """
123
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000124 def do_index(self):
Guido van Rossum74427e51997-05-21 23:43:39 +0000125 self.prologue("Python FAQ Index")
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000126 names = os.listdir(os.curdir)
127 names.sort()
128 section = None
129 for name in names:
130 headers, text = self.read(name)
131 if headers:
132 title = headers['title']
133 i = string.find(title, '.')
134 nsec = title[:i]
135 if nsec != section:
136 if section:
137 print """
138 <P>
139 <LI><A HREF="faq.py?req=add&amp;section=%s"
140 >Add new entry</A> (at this point)
141 </UL>
142 """ % section
143 section = nsec
Guido van Rossum4888c7e1997-05-23 15:55:19 +0000144 if SECTIONS.has_key(section):
145 stitle = SECTIONS[section]
146 else:
147 stitle = ""
148 print "<H2>Section %s. %s</H2>" % (section, stitle)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000149 print "<UL>"
150 print '<LI><A HREF="faq.py?req=show&name=%s">%s</A>' % (
151 name, cgi.escape(title))
152 if section:
153 print """
154 <P>
155 <LI><A HREF="faq.py?req=add&amp;section=%s">Add new entry</A>
156 (at this point)
157 </UL>
158 """ % section
159 else:
160 print "No FAQ entries?!?!"
161
162 def do_show(self):
Guido van Rossumaf5be951997-05-22 16:57:50 +0000163 self.prologue("Python FAQ Entry")
164 print "<HR>"
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000165 name = self.name
166 headers, text = self.read(name)
167 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000168 self.error("Invalid file name", name)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000169 return
Guido van Rossumed531fd1997-05-22 15:21:57 +0000170 self.show(name, headers['title'], text)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000171
172 def do_all(self):
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000173 import fnmatch, stat
Guido van Rossum74427e51997-05-21 23:43:39 +0000174 self.prologue("The Whole Python FAQ")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000175 names = os.listdir(os.curdir)
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000176 lastmtime = 0
177 for name in names:
178 if not fnmatch.fnmatch(name, NAMEPAT):
179 continue
180 try:
181 st = os.stat(name)
182 except os.error:
183 continue
184 lastmtime = max(lastmtime, st[stat.ST_MTIME])
185 if lastmtime:
186 print time.strftime("Last changed on %c %Z",
187 time.localtime(lastmtime))
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000188 names.sort()
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000189 section = None
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000190 print "<HR>"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000191 for name in names:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000192 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000193 if headers:
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000194 title = headers['title']
195 i = string.find(title, '.')
196 nsec = title[:i]
197 if nsec != section:
198 section = nsec
Guido van Rossum4888c7e1997-05-23 15:55:19 +0000199 if SECTIONS.has_key(section):
200 stitle = SECTIONS[section]
201 else:
202 stitle = ""
203 print "<H1>Section %s. %s</H1>" % (section, stitle)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000204 print "<HR>"
Guido van Rossum4888c7e1997-05-23 15:55:19 +0000205 self.show(name, title, text, edit=(self.edit != 'no'))
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000206 if not section:
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000207 print "No FAQ entries?!?!"
208
209 def do_roulette(self):
210 import whrandom
Guido van Rossum74427e51997-05-21 23:43:39 +0000211 self.prologue("Python FAQ Roulette")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000212 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000213 Please check the correctness of the entry below.
214 If you find any problems, please edit the entry.
215 <P>
216 <HR>
217 """
218 names = os.listdir(os.curdir)
219 while names:
220 name = whrandom.choice(names)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000221 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000222 if headers:
Guido van Rossumed531fd1997-05-22 15:21:57 +0000223 self.show(name, headers['title'], text)
224 print "<P>Use `Reload' to show another one."
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000225 break
226 else:
227 names.remove(name)
228 else:
229 print "No FAQ entries?!?!"
230
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000231 def do_recent(self):
232 import fnmatch, stat
233 names = os.listdir(os.curdir)
234 now = time.time()
235 list = []
236 for name in names:
237 if not fnmatch.fnmatch(name, NAMEPAT):
238 continue
239 try:
240 st = os.stat(name)
241 except os.error:
242 continue
243 tuple = (st[stat.ST_MTIME], name)
244 list.append(tuple)
245 list.sort()
246 list.reverse()
Guido van Rossum74427e51997-05-21 23:43:39 +0000247 self.prologue("Python FAQ, Most Recently Modified First")
248 print "<HR>"
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000249 n = 0
250 for (mtime, name) in list:
251 headers, text = self.read(name)
Guido van Rossumc6447521997-05-23 00:50:01 +0000252 if headers and headers.has_key('last-changed-date'):
Guido van Rossumed531fd1997-05-22 15:21:57 +0000253 self.show(name, headers['title'], text)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000254 n = n+1
255 if not n:
256 print "No FAQ entries?!?!"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000257
258 def do_query(self):
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000259 query = self.query
260 if not query:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000261 self.error("No query string")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000262 return
Guido van Rossumaf5be951997-05-22 16:57:50 +0000263 import regex
264 self.prologue("Python FAQ Query Results")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000265 p = regex.compile(query, regex.casefold)
266 names = os.listdir(os.curdir)
267 names.sort()
268 print "<HR>"
269 n = 0
270 for name in names:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000271 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000272 if headers:
273 title = headers['title']
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000274 if p.search(title) >= 0 or p.search(text) >= 0:
Guido van Rossumed531fd1997-05-22 15:21:57 +0000275 self.show(name, title, text)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000276 n = n+1
277 if not n:
278 print "No hits."
279
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000280 def do_add(self):
281 section = self.section
282 if not section:
Guido van Rossum74427e51997-05-21 23:43:39 +0000283 self.prologue("How to add a new FAQ entry")
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000284 print """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000285 Go to the <A HREF="faq.py?req=index">FAQ index</A>
286 and click on the "Add new entry" link at the end
287 of the section to which you want to add the entry.
288 """
289 return
290 try:
291 nsec = string.atoi(section)
292 except ValueError:
293 print "Bad section number", nsec
294 names = os.listdir(os.curdir)
295 max = 0
296 import regex
297 prog = regex.compile(NAMEREG)
298 for name in names:
299 if prog.match(name) >= 0:
300 s1, s2 = prog.group(1, 2)
301 n1, n2 = string.atoi(s1), string.atoi(s2)
302 if n1 == nsec:
303 if n2 > max:
304 max = n2
305 if not max:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000306 self.error("Can't add new sections yet.")
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000307 return
308 num = max+1
309 name = "faq%02d.%03d.htp" % (nsec, num)
310 self.name = name
311 self.add = "yes"
312 self.number = str(num)
313 self.do_edit()
314
Guido van Rossumaf5be951997-05-22 16:57:50 +0000315 def do_delete(self):
316 self.prologue("How to delete a FAQ entry")
317 print """
318 At the moment, there's no direct way to delete entries.
319 This is because the entry numbers are also their
320 unique identifiers -- it's a bad idea to renumber entries.
321 <P>
322 If you really think an entry needs to be deleted,
323 change the title to "(deleted)" and make the body
324 empty (keep the entry number in the title though).
325 """
326
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000327 def do_edit(self):
328 name = self.name
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000329 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000330 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000331 self.error("Invalid file name", name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000332 return
Guido van Rossum74427e51997-05-21 23:43:39 +0000333 self.prologue("Python FAQ Edit Form")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000334 title = headers['title']
Guido van Rossum74427e51997-05-21 23:43:39 +0000335 version = self.getversion(name)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000336 print "<FORM METHOD=POST ACTION=faq.py>"
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000337 self.showedit(name, title, text)
338 if self.add:
339 print """
340 <INPUT TYPE=hidden NAME=add VALUE=%s>
341 <INPUT TYPE=hidden NAME=section VALUE=%s>
342 <INPUT TYPE=hidden NAME=number VALUE=%s>
343 """ % (self.add, self.section, self.number)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000344 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000345 <INPUT TYPE=submit VALUE="Review Edit">
346 <INPUT TYPE=hidden NAME=req VALUE=review>
347 <INPUT TYPE=hidden NAME=name VALUE=%s>
Guido van Rossum74427e51997-05-21 23:43:39 +0000348 <INPUT TYPE=hidden NAME=version VALUE=%s>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000349 </FORM>
350 <HR>
Guido van Rossum74427e51997-05-21 23:43:39 +0000351 """ % (name, version)
Guido van Rossumed531fd1997-05-22 15:21:57 +0000352 self.show(name, title, text, edit=0)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000353
354 def do_review(self):
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000355 if self.commit:
356 self.checkin()
357 return
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000358 name = self.name
359 text = self.text
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000360 title = self.title
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000361 headers, oldtext = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000362 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000363 self.error("Invalid file name", name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000364 return
Guido van Rossumaf5be951997-05-22 16:57:50 +0000365 if self.author and '@' in self.email:
366 self.set_cookie(self.author, self.email)
Guido van Rossum74427e51997-05-21 23:43:39 +0000367 self.prologue("Python FAQ Review Form")
368 print "<HR>"
Guido van Rossumed531fd1997-05-22 15:21:57 +0000369 self.show(name, title, text, edit=0)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000370 print "<FORM METHOD=POST ACTION=faq.py>"
371 if self.log and self.author and '@' in self.email:
372 print """
373 <INPUT TYPE=submit NAME=commit VALUE="Commit">
374 Click this button to commit the change.
375 <P>
376 <HR>
377 <P>
378 """
379 else:
380 print """
381 To commit this change, please enter your name,
382 email and a log message in the form below.
383 <P>
384 <HR>
385 <P>
386 """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000387 self.showedit(name, title, text)
388 if self.add:
389 print """
390 <INPUT TYPE=hidden NAME=add VALUE=%s>
391 <INPUT TYPE=hidden NAME=section VALUE=%s>
392 <INPUT TYPE=hidden NAME=number VALUE=%s>
393 """ % (self.add, self.section, self.number)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000394 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000395 <BR>
396 <INPUT TYPE=submit VALUE="Review Edit">
397 <INPUT TYPE=hidden NAME=req VALUE=review>
398 <INPUT TYPE=hidden NAME=name VALUE=%s>
Guido van Rossum74427e51997-05-21 23:43:39 +0000399 <INPUT TYPE=hidden NAME=version VALUE=%s>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000400 </FORM>
401 <HR>
Guido van Rossum74427e51997-05-21 23:43:39 +0000402 """ % (name, self.version)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000403
Guido van Rossumf701bf11997-05-21 22:25:56 +0000404 def do_info(self):
405 name = self.name
406 headers, text = self.read(name)
407 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000408 self.error("Invalid file name", name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000409 return
Guido van Rossumc6447521997-05-23 00:50:01 +0000410 self.prologue("Info for %s" % name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000411 print '<PRE>'
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000412 p = os.popen("/depot/gnu/plat/bin/rlog -r %s </dev/null 2>&1" %
413 self.name)
414 output = p.read()
415 p.close()
416 print cgi.escape(output)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000417 print '</PRE>'
418 print '<A HREF="faq.py?req=rlog&name=%s">View full rcs log</A>' % name
419
420 def do_rlog(self):
421 name = self.name
422 headers, text = self.read(name)
423 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000424 self.error("Invalid file name", name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000425 return
Guido van Rossumc6447521997-05-23 00:50:01 +0000426 self.prologue("RCS log for %s" % name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000427 print '<PRE>'
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000428 p = os.popen("/depot/gnu/plat/bin/rlog %s </dev/null 2>&1" % self.name)
429 output = p.read()
430 p.close()
431 print cgi.escape(output)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000432 print '</PRE>'
433
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000434 def checkin(self):
435 import regsub, time, tempfile
436 name = self.name
437
438 headers, oldtext = self.read(name)
439 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000440 self.error("Invalid file name", name)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000441 return
Guido van Rossum74427e51997-05-21 23:43:39 +0000442 version = self.version
443 curversion = self.getversion(name)
444 if version != curversion:
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000445 self.error(
446 "Version conflict.",
447 "You edited version %s but current version is %s." % (
448 version, curversion),
449 """
450 <P>
451 The two most common causes of this problem are:
452 <UL>
453 <LI>After committing a change, you went back in your browser,
454 edited the entry some more, and clicked Commit again.
455 <LI>Someone else started editing the same entry and committed
456 before you did.
457 </UL>
458 <P>
459 """,
460 '<A HREF="faq.py?req=show&name=%s"' % name,
461 '>Click here to reload the entry and try again.</A>')
Guido van Rossum74427e51997-05-21 23:43:39 +0000462 return
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000463 text = self.text
464 title = self.title
465 author = self.author
466 email = self.email
467 log = self.log
468 text = regsub.gsub("\r\n", "\n", text)
469 log = regsub.gsub("\r\n", "\n", log)
470 author = string.join(string.split(author))
471 email = string.join(string.split(email))
472 title = string.join(string.split(title))
473 oldtitle = headers['title']
474 oldtitle = string.join(string.split(oldtitle))
475 text = string.strip(text)
476 oldtext = string.strip(oldtext)
477 if text == oldtext and title == oldtitle:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000478 self.error("No changes.")
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000479 return
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000480 # Check that the FAQ entry number didn't change
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000481 if string.split(title)[:1] != string.split(oldtitle)[:1]:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000482 self.error("Don't change the FAQ entry number please.")
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000483 return
484 remhost = os.environ["REMOTE_HOST"]
485 remaddr = os.environ["REMOTE_ADDR"]
486 try:
487 os.unlink(name + "~")
488 except os.error:
489 pass
490 try:
491 os.rename(name, name + "~")
492 except os.error:
493 pass
494 try:
495 os.unlink(name)
496 except os.error:
497 pass
498 try:
499 f = open(name, "w")
500 except IOError, msg:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000501 self.error("Can't open", name, "for writing:", cgi.escape(str(msg)))
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000502 return
503 now = time.ctime(time.time())
504 f.write("Title: %s\n" % title)
505 f.write("Last-Changed-Date: %s\n" % now)
506 f.write("Last-Changed-Author: %s\n" % author)
507 f.write("Last-Changed-Email: %s\n" % email)
508 f.write("Last-Changed-Remote-Host: %s\n" % remhost)
509 f.write("Last-Changed-Remote-Address: %s\n" % remaddr)
510 keys = headers.keys()
511 keys.sort()
512 keys.remove('title')
513 for key in keys:
514 if key[:13] != 'last-changed-':
515 f.write("%s: %s\n" % (string.capwords(key, '-'),
516 headers[key]))
517 f.write("\n")
518 f.write(text)
519 f.write("\n")
520 f.close()
521
522 tfn = tempfile.mktemp()
523 f = open(tfn, "w")
524 f.write("Last-Changed-Date: %s\n" % now)
525 f.write("Last-Changed-Author: %s\n" % author)
526 f.write("Last-Changed-Email: %s\n" % email)
527 f.write("Last-Changed-Remote-Host: %s\n" % remhost)
528 f.write("Last-Changed-Remote-Address: %s\n" % remaddr)
529 f.write("\n")
530 f.write(log)
531 f.write("\n")
532 f.close()
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000533
534 # Do this for show() below
535 self.headers = {
536 'title': title,
537 'last-changed-date': now,
538 'last-changed-author': author,
539 'last-changed-email': email,
540 'last-changed-remote-host': remhost,
541 'last-changed-remote-address': remaddr,
542 }
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000543
544 p = os.popen("""
545 /depot/gnu/plat/bin/rcs -l %s </dev/null 2>&1
546 /depot/gnu/plat/bin/ci -u %s <%s 2>&1
547 rm -f %s
548 """ % (name, name, tfn, tfn))
549 output = p.read()
550 sts = p.close()
551 if not sts:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000552 self.set_cookie(author, email)
Guido van Rossum74427e51997-05-21 23:43:39 +0000553 self.prologue("Python FAQ Entry Edited")
554 print "<HR>"
Guido van Rossumed531fd1997-05-22 15:21:57 +0000555 self.show(name, title, text)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000556 if output:
557 print "<PRE>%s</PRE>" % cgi.escape(output)
558 else:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000559 self.error("Python FAQ Entry Commit Failed",
560 "Exit status 0x%04x" % sts)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000561 if output:
562 print "<PRE>%s</PRE>" % cgi.escape(output)
563
Guido van Rossumaf5be951997-05-22 16:57:50 +0000564 def set_cookie(self, author, email):
565 name = "Python-FAQ-ID"
566 value = "%s;%s" % (author, email)
567 import urllib
568 value = urllib.quote(value)
569 print "Set-Cookie: %s=%s; path=/cgi-bin/;" % (name, value),
Guido van Rossum1d579811997-05-23 19:18:35 +0000570 import time
571 now = time.time()
572 then = now + 28 * 24 * 3600
573 gmt = time.gmtime(then)
574 print time.strftime("expires=%a, %d-%b-%x %X GMT", gmt)
Guido van Rossumaf5be951997-05-22 16:57:50 +0000575
576 def get_cookie(self):
577 if not os.environ.has_key('HTTP_COOKIE'):
578 return "", ""
579 raw = os.environ['HTTP_COOKIE']
580 words = string.split(raw, ';')
581 cookies = {}
582 for word in words:
583 i = string.find(word, '=')
584 if i >= 0:
585 key, value = word[:i], word[i+1:]
586 cookies[key] = value
587 if not cookies.has_key('Python-FAQ-ID'):
588 return "", ""
589 value = cookies['Python-FAQ-ID']
590 import urllib
591 value = urllib.unquote(value)
592 i = string.rfind(value, ';')
593 author, email = value[:i], value[i+1:]
594 return author, email
595
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000596 def showedit(self, name, title, text):
Guido van Rossumaf5be951997-05-22 16:57:50 +0000597 author = self.author
598 email = self.email
599 if not author or not email:
600 a, e = self.get_cookie()
601 author = author or a
602 email = email or e
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000603 print """
Guido van Rossumed531fd1997-05-22 15:21:57 +0000604 Title: <INPUT TYPE=text SIZE=70 NAME=title VALUE="%s"><BR>
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000605 <TEXTAREA COLS=80 ROWS=20 NAME=text>%s</TEXTAREA>
606 """ % (self.escape(title), cgi.escape(string.strip(text)))
607 print """
608 <BR>
609 Log message (reason for the change):<BR>
610 <TEXTAREA COLS=80 ROWS=5 NAME=log>%s\n</TEXTAREA>
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000611 <BR>
612 Please provide the following information for logging purposes:
613 <BR>
Guido van Rossumed531fd1997-05-22 15:21:57 +0000614 <CODE>Name : </CODE><INPUT TYPE=text SIZE=40 NAME=author VALUE="%s">
615 <BR>
616 <CODE>Email: </CODE><INPUT TYPE=text SIZE=40 NAME=email VALUE="%s">
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000617 """ % (self.escape(self.log), self.escape(author), self.escape(email))
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000618
619 def escape(self, s):
620 import regsub
621 if '&' in s:
622 s = regsub.gsub("&", "&amp;", s) # Must be done first!
623 if '<' in s:
624 s = regsub.gsub("<", "&lt;", s)
625 if '>' in s:
626 s = regsub.gsub(">", "&gt;", s)
627 if '"' in s:
628 s = regsub.gsub('"', "&quot;", s)
629 return s
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000630
631 def showheaders(self, headers):
632 print "<UL>"
633 keys = map(string.lower, headers.keys())
634 keys.sort()
635 for key in keys:
636 print "<LI><B>%s:</B> %s" % (string.capwords(key, '-'),
637 headers[key] or '')
638 print "</UL>"
639
Guido van Rossumed531fd1997-05-22 15:21:57 +0000640 headers = None
641
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000642 def read(self, name):
Guido van Rossumed531fd1997-05-22 15:21:57 +0000643 self.headers = None
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000644 import fnmatch, rfc822
645 if not fnmatch.fnmatch(name, NAMEPAT):
646 return None, None
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000647 if self.add:
648 try:
649 fname = "faq%02d.%03d.htp" % (string.atoi(self.section),
650 string.atoi(self.number))
651 except ValueError:
652 return None, None
653 if fname != name:
654 return None, None
655 headers = {'title': "%s.%s. " % (self.section, self.number)}
656 text = ""
Guido van Rossumed531fd1997-05-22 15:21:57 +0000657 else:
658 f = open(name)
659 headers = rfc822.Message(f)
660 text = f.read()
661 f.close()
662 self.headers = headers
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000663 return headers, text
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000664
Guido van Rossumed531fd1997-05-22 15:21:57 +0000665 def show(self, name, title, text, edit=1):
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000666 print "<H2>%s</H2>" % cgi.escape(title)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000667 pre = 0
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000668 for line in string.split(text, '\n'):
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000669 if not string.strip(line):
670 if pre:
671 print '</PRE>'
672 pre = 0
673 else:
674 print '<P>'
675 else:
Guido van Rossum5527db51997-05-23 04:44:30 +0000676 if line[0] not in string.whitespace:
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000677 if pre:
678 print '</PRE>'
679 pre = 0
680 else:
681 if not pre:
682 print '<PRE>'
683 pre = 1
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000684 if '/' in line or '@' in line:
685 line = self.translate(line)
Guido van Rossuma0e9a6d1997-05-23 18:13:58 +0000686 elif '<' in line or '&' in line:
687 line = cgi.escape(line)
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000688 if not pre and '*' in line:
689 line = self.emphasize(line)
690 print line
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000691 if pre:
692 print '</PRE>'
693 pre = 0
694 print '<P>'
695 if edit:
Guido van Rossumf701bf11997-05-21 22:25:56 +0000696 print """
697 <A HREF="faq.py?req=edit&name=%s">Edit this entry</A> /
Guido van Rossum1d579811997-05-23 19:18:35 +0000698 <A HREF="faq.py?req=info&name=%s" TARGET=rlog>Log info</A>
Guido van Rossumf701bf11997-05-21 22:25:56 +0000699 """ % (name, name)
Guido van Rossumed531fd1997-05-22 15:21:57 +0000700 if self.headers:
701 try:
702 date = self.headers['last-changed-date']
703 author = self.headers['last-changed-author']
704 email = self.headers['last-changed-email']
705 except KeyError:
706 pass
707 else:
Guido van Rossum1d579811997-05-23 19:18:35 +0000708 s = '/ Last changed on %s by <A HREF="mailto:%s">%s</A>'
Guido van Rossumed531fd1997-05-22 15:21:57 +0000709 print s % (date, email, author)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000710 print '<P>'
711 print "<HR>"
712
Guido van Rossum74427e51997-05-21 23:43:39 +0000713 def getversion(self, name):
714 p = os.popen("/depot/gnu/plat/bin/rlog -h %s </dev/null 2>&1" % name)
Guido van Rossum64099e91997-05-22 15:49:23 +0000715 head = "*new*"
Guido van Rossum74427e51997-05-21 23:43:39 +0000716 while 1:
717 line = p.readline()
718 if not line:
719 break
720 if line[:5] == 'head:':
721 head = string.strip(line[5:])
722 p.close()
723 return head
724
725 def prologue(self, title):
726 title = cgi.escape(title)
Guido van Rossumaf5be951997-05-22 16:57:50 +0000727 print '''
Guido van Rossum74427e51997-05-21 23:43:39 +0000728 <HTML>
729 <HEAD>
730 <TITLE>%s</TITLE>
731 </HEAD>
732 <BODY BACKGROUND="http://www.python.org/pics/RedShort.gif"
733 BGCOLOR="#FFFFFF"
734 TEXT="#000000"
735 LINK="#AA0000"
736 VLINK="#906A6A">
737 <H1>%s</H1>
738 ''' % (title, title)
739
Guido van Rossumaf5be951997-05-22 16:57:50 +0000740 def error(self, *messages):
741 self.prologue("Python FAQ error")
742 print "Sorry, an error occurred:<BR>"
743 for message in messages:
744 print message,
745 print
746
Guido van Rossum74427e51997-05-21 23:43:39 +0000747 def epilogue(self):
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000748 if self.edit == 'no':
749 global wanttime
750 wanttime = 0
751 else:
752 print '''
753 <P>
754 <HR>
755 <A HREF="http://www.python.org">Python home</A> /
Guido van Rossuma0e9a6d1997-05-23 18:13:58 +0000756 <A HREF="faq.py?req=frontpage">FAQ home</A> /
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000757 Feedback to <A HREF="mailto:guido@python.org">GvR</A>
758 '''
Guido van Rossum74427e51997-05-21 23:43:39 +0000759 print '''
Guido van Rossum74427e51997-05-21 23:43:39 +0000760 </BODY>
761 </HTML>
762 '''
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000763
Guido van Rossum5527db51997-05-23 04:44:30 +0000764 translate_prog = None
765
766 def translate(self, text):
767 if not self.translate_prog:
768 import regex
769 url = '\(http\|ftp\)://[^ \t\r\n]*'
770 email = '\<[-a-zA-Z0-9._]+@[-a-zA-Z0-9._]+'
771 self.translate_prog = prog = regex.compile(url + "\|" + email)
772 else:
773 prog = self.translate_prog
774 i = 0
775 list = []
776 while 1:
777 j = prog.search(text, i)
778 if j < 0:
779 break
780 list.append(cgi.escape(text[i:j]))
781 i = j
782 url = prog.group(0)
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000783 while url[-1] in ");:,.?'\"":
Guido van Rossum5527db51997-05-23 04:44:30 +0000784 url = url[:-1]
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000785 url = self.escape(url)
Guido van Rossum5527db51997-05-23 04:44:30 +0000786 if ':' in url:
787 repl = '<A HREF="%s">%s</A>' % (url, url)
788 else:
789 repl = '<A HREF="mailto:%s">&lt;%s&gt;</A>' % (url, url)
790 list.append(repl)
791 i = i + len(url)
792 j = len(text)
793 list.append(cgi.escape(text[i:j]))
794 return string.join(list, '')
795
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000796 emphasize_prog = None
797
798 def emphasize(self, line):
799 import regsub
800 if not self.emphasize_prog:
801 import regex
802 pat = "\*\([a-zA-Z]+\)\*"
803 self.emphasize_prog = prog = regex.compile(pat)
804 else:
805 prog = self.emphasize_prog
806 return regsub.gsub(prog, "<I>\\1</I>", line)
807
Guido van Rossumaf5be951997-05-22 16:57:50 +0000808print "Content-type: text/html"
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000809dt = 0
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000810wanttime = 0
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000811try:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000812 import time
813 t1 = time.time()
Guido van Rossum74427e51997-05-21 23:43:39 +0000814 import cgi, string, os, sys
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000815 x = FAQServer()
816 x.main()
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000817 t2 = time.time()
818 dt = t2-t1
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000819 wanttime = 1
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000820except:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000821 print "\n<HR>Sorry, an error occurred"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000822 cgi.print_exception()
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000823if wanttime:
824 print "<BR>(running time = %s seconds)" % str(round(dt, 3))
Guido van Rossumed531fd1997-05-22 15:21:57 +0000825
826# The following bootstrap script must be placed in cgi-bin/faq.py:
827BOOTSTRAP = """
828#! /usr/local/bin/python
829FAQDIR = "/usr/people/guido/python/FAQ"
830import os, sys
831os.chdir(FAQDIR)
832sys.path.insert(0, os.curdir)
833import faqmain
834"""