blob: fcb82e9efd35e349c296066af05f8a233524a991 [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 Rossuma78a3c31997-05-23 22:29:24 +000047PASSWORD = "Spam"
48
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000049NAMEPAT = "faq??.???.htp"
Guido van Rossumd7bfa801997-05-21 21:31:39 +000050NAMEREG = "^faq\([0-9][0-9]\)\.\([0-9][0-9][0-9]\)\.htp$"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000051
Guido van Rossum4888c7e1997-05-23 15:55:19 +000052SECTIONS = {
53 "1": "General information and availability",
54 "2": "Python in the real world",
55 "3": "Building Python and Other Known Bugs",
56 "4": "Programming in Python",
57 "5": "Extending Python",
58 "6": "Python's design",
59 "7": "Using Python on non-UNIX platforms",
60}
61
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000062class FAQServer:
63
64 def __init__(self):
65 pass
66
67 def main(self):
68 self.form = cgi.FieldStorage()
69 req = self.req or 'frontpage'
70 try:
71 method = getattr(self, 'do_%s' % req)
72 except AttributeError:
73 print "Unrecognized request type", req
74 else:
75 method()
Guido van Rossum74427e51997-05-21 23:43:39 +000076 self.epilogue()
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000077
Guido van Rossum3c3354c1997-05-21 16:52:18 +000078 KEYS = ['req', 'query', 'name', 'text', 'commit', 'title',
Guido van Rossum74427e51997-05-21 23:43:39 +000079 'author', 'email', 'log', 'section', 'number', 'add',
Guido van Rossuma78a3c31997-05-23 22:29:24 +000080 'version', 'edit', 'password']
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000081
82 def __getattr__(self, key):
83 if key not in self.KEYS:
84 raise AttributeError
Guido van Rossum3c3354c1997-05-21 16:52:18 +000085 try:
Guido van Rossumed531fd1997-05-22 15:21:57 +000086 form = self.form
87 try:
88 item = form[key]
89 except TypeError, msg:
90 raise KeyError, msg, sys.exc_traceback
Guido van Rossum3c3354c1997-05-21 16:52:18 +000091 except KeyError:
92 return ''
Guido van Rossumaf5be951997-05-22 16:57:50 +000093 value = self.form[key].value
94 value = string.strip(value)
95 setattr(self, key, value)
96 return value
Guido van Rossumadb3a9d1997-05-21 07:24:50 +000097
98 def do_frontpage(self):
Guido van Rossum9c6ceda1997-05-23 22:44:01 +000099 self.prologue("Python FAQ Wizard (beta test)")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000100 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000101 <UL>
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000102 <LI><A HREF="faq.py?req=index">FAQ index</A>
103 <LI><A HREF="faq.py?req=all">The whole FAQ</A>
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000104 <LI><A HREF="faq.py?req=roulette">FAQ roulette</A>
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000105 <LI><A HREF="faq.py?req=recent">Recently changed FAQ entries</A>
106 <LI><A HREF="faq.py?req=add">Add a new FAQ entry</A>
Guido van Rossumaf5be951997-05-22 16:57:50 +0000107 <LI><A HREF="faq.py?req=delete">Delete a FAQ entry</A>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000108 </UL>
109
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000110 <HR>
111
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000112 <H2>Search the FAQ</H2>
113
Guido van Rossuma0e9a6d1997-05-23 18:13:58 +0000114 <FORM ACTION="faq.py">
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000115 <INPUT TYPE=text NAME=query>
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000116 <INPUT TYPE=submit VALUE="Search"><BR>
117 (Case insensitive regular expressions.)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000118 <INPUT TYPE=hidden NAME=req VALUE=query>
119 </FORM>
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000120 <HR>
121 <P>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000122 Disclaimer: these pages are intended to be edited by anyone.
123 Please exercise discretion when editing, don't be rude, etc.
124 """
125
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000126 def do_index(self):
Guido van Rossum74427e51997-05-21 23:43:39 +0000127 self.prologue("Python FAQ Index")
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000128 names = os.listdir(os.curdir)
129 names.sort()
130 section = None
131 for name in names:
132 headers, text = self.read(name)
133 if headers:
134 title = headers['title']
135 i = string.find(title, '.')
136 nsec = title[:i]
137 if nsec != section:
138 if section:
139 print """
140 <P>
141 <LI><A HREF="faq.py?req=add&amp;section=%s"
142 >Add new entry</A> (at this point)
143 </UL>
144 """ % section
145 section = nsec
Guido van Rossum4888c7e1997-05-23 15:55:19 +0000146 if SECTIONS.has_key(section):
147 stitle = SECTIONS[section]
148 else:
149 stitle = ""
150 print "<H2>Section %s. %s</H2>" % (section, stitle)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000151 print "<UL>"
152 print '<LI><A HREF="faq.py?req=show&name=%s">%s</A>' % (
153 name, cgi.escape(title))
154 if section:
155 print """
156 <P>
157 <LI><A HREF="faq.py?req=add&amp;section=%s">Add new entry</A>
158 (at this point)
159 </UL>
160 """ % section
161 else:
162 print "No FAQ entries?!?!"
163
164 def do_show(self):
Guido van Rossumaf5be951997-05-22 16:57:50 +0000165 self.prologue("Python FAQ Entry")
166 print "<HR>"
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000167 name = self.name
168 headers, text = self.read(name)
169 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000170 self.error("Invalid file name", name)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000171 return
Guido van Rossumed531fd1997-05-22 15:21:57 +0000172 self.show(name, headers['title'], text)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000173
174 def do_all(self):
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000175 import fnmatch, stat
Guido van Rossum74427e51997-05-21 23:43:39 +0000176 self.prologue("The Whole Python FAQ")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000177 names = os.listdir(os.curdir)
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000178 lastmtime = 0
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 lastmtime = max(lastmtime, st[stat.ST_MTIME])
187 if lastmtime:
188 print time.strftime("Last changed on %c %Z",
189 time.localtime(lastmtime))
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000190 names.sort()
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000191 section = None
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000192 print "<HR>"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000193 for name in names:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000194 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000195 if headers:
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000196 title = headers['title']
197 i = string.find(title, '.')
198 nsec = title[:i]
199 if nsec != section:
200 section = nsec
Guido van Rossum4888c7e1997-05-23 15:55:19 +0000201 if SECTIONS.has_key(section):
202 stitle = SECTIONS[section]
203 else:
204 stitle = ""
205 print "<H1>Section %s. %s</H1>" % (section, stitle)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000206 print "<HR>"
Guido van Rossum4888c7e1997-05-23 15:55:19 +0000207 self.show(name, title, text, edit=(self.edit != 'no'))
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000208 if not section:
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000209 print "No FAQ entries?!?!"
210
211 def do_roulette(self):
212 import whrandom
Guido van Rossum74427e51997-05-21 23:43:39 +0000213 self.prologue("Python FAQ Roulette")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000214 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000215 Please check the correctness of the entry below.
216 If you find any problems, please edit the entry.
217 <P>
218 <HR>
219 """
220 names = os.listdir(os.curdir)
221 while names:
222 name = whrandom.choice(names)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000223 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000224 if headers:
Guido van Rossumed531fd1997-05-22 15:21:57 +0000225 self.show(name, headers['title'], text)
226 print "<P>Use `Reload' to show another one."
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000227 break
228 else:
229 names.remove(name)
230 else:
231 print "No FAQ entries?!?!"
232
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000233 def do_recent(self):
234 import fnmatch, stat
235 names = os.listdir(os.curdir)
236 now = time.time()
237 list = []
238 for name in names:
239 if not fnmatch.fnmatch(name, NAMEPAT):
240 continue
241 try:
242 st = os.stat(name)
243 except os.error:
244 continue
245 tuple = (st[stat.ST_MTIME], name)
246 list.append(tuple)
247 list.sort()
248 list.reverse()
Guido van Rossum74427e51997-05-21 23:43:39 +0000249 self.prologue("Python FAQ, Most Recently Modified First")
250 print "<HR>"
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000251 n = 0
252 for (mtime, name) in list:
253 headers, text = self.read(name)
Guido van Rossumc6447521997-05-23 00:50:01 +0000254 if headers and headers.has_key('last-changed-date'):
Guido van Rossumed531fd1997-05-22 15:21:57 +0000255 self.show(name, headers['title'], text)
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000256 n = n+1
257 if not n:
258 print "No FAQ entries?!?!"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000259
260 def do_query(self):
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000261 query = self.query
262 if not query:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000263 self.error("No query string")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000264 return
Guido van Rossumaf5be951997-05-22 16:57:50 +0000265 import regex
266 self.prologue("Python FAQ Query Results")
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000267 p = regex.compile(query, regex.casefold)
268 names = os.listdir(os.curdir)
269 names.sort()
270 print "<HR>"
271 n = 0
272 for name in names:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000273 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000274 if headers:
275 title = headers['title']
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000276 if p.search(title) >= 0 or p.search(text) >= 0:
Guido van Rossumed531fd1997-05-22 15:21:57 +0000277 self.show(name, title, text)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000278 n = n+1
279 if not n:
280 print "No hits."
281
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000282 def do_add(self):
283 section = self.section
284 if not section:
Guido van Rossum74427e51997-05-21 23:43:39 +0000285 self.prologue("How to add a new FAQ entry")
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000286 print """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000287 Go to the <A HREF="faq.py?req=index">FAQ index</A>
288 and click on the "Add new entry" link at the end
289 of the section to which you want to add the entry.
290 """
291 return
292 try:
293 nsec = string.atoi(section)
294 except ValueError:
295 print "Bad section number", nsec
296 names = os.listdir(os.curdir)
297 max = 0
298 import regex
299 prog = regex.compile(NAMEREG)
300 for name in names:
301 if prog.match(name) >= 0:
302 s1, s2 = prog.group(1, 2)
303 n1, n2 = string.atoi(s1), string.atoi(s2)
304 if n1 == nsec:
305 if n2 > max:
306 max = n2
307 if not max:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000308 self.error("Can't add new sections yet.")
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000309 return
310 num = max+1
311 name = "faq%02d.%03d.htp" % (nsec, num)
312 self.name = name
313 self.add = "yes"
314 self.number = str(num)
315 self.do_edit()
316
Guido van Rossumaf5be951997-05-22 16:57:50 +0000317 def do_delete(self):
318 self.prologue("How to delete a FAQ entry")
319 print """
320 At the moment, there's no direct way to delete entries.
321 This is because the entry numbers are also their
322 unique identifiers -- it's a bad idea to renumber entries.
323 <P>
324 If you really think an entry needs to be deleted,
325 change the title to "(deleted)" and make the body
326 empty (keep the entry number in the title though).
327 """
328
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000329 def do_edit(self):
330 name = self.name
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000331 headers, text = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000332 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000333 self.error("Invalid file name", name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000334 return
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000335 self.prologue("Python FAQ Edit Wizard - Edit Form")
336 print '<A HREF="/python/faqhelp.html">Click for Help</A>'
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000337 title = headers['title']
Guido van Rossum74427e51997-05-21 23:43:39 +0000338 version = self.getversion(name)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000339 print "<FORM METHOD=POST ACTION=faq.py>"
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000340 self.showedit(name, title, text)
341 if self.add:
342 print """
343 <INPUT TYPE=hidden NAME=add VALUE=%s>
344 <INPUT TYPE=hidden NAME=section VALUE=%s>
345 <INPUT TYPE=hidden NAME=number VALUE=%s>
346 """ % (self.add, self.section, self.number)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000347 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000348 <INPUT TYPE=submit VALUE="Review Edit">
349 <INPUT TYPE=hidden NAME=req VALUE=review>
350 <INPUT TYPE=hidden NAME=name VALUE=%s>
Guido van Rossum74427e51997-05-21 23:43:39 +0000351 <INPUT TYPE=hidden NAME=version VALUE=%s>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000352 </FORM>
353 <HR>
Guido van Rossum74427e51997-05-21 23:43:39 +0000354 """ % (name, version)
Guido van Rossumed531fd1997-05-22 15:21:57 +0000355 self.show(name, title, text, edit=0)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000356
357 def do_review(self):
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000358 if self.commit:
359 self.checkin()
360 return
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000361 name = self.name
362 text = self.text
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000363 title = self.title
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000364 headers, oldtext = self.read(name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000365 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000366 self.error("Invalid file name", name)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000367 return
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000368 if self.author or '@' in self.email or self.password:
369 self.set_cookie(self.author, self.email, self.password)
370 self.prologue("Python FAQ Edit Wizard - Review Form")
371 print '<A HREF="/python/faqhelp.html">Click for Help</A>'
Guido van Rossum74427e51997-05-21 23:43:39 +0000372 print "<HR>"
Guido van Rossumed531fd1997-05-22 15:21:57 +0000373 self.show(name, title, text, edit=0)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000374 print "<FORM METHOD=POST ACTION=faq.py>"
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000375 if self.password == PASSWORD \
376 and self.log and self.author and '@' in self.email:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000377 print """
378 <INPUT TYPE=submit NAME=commit VALUE="Commit">
379 Click this button to commit the change.
380 <P>
381 <HR>
382 <P>
383 """
384 else:
385 print """
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000386 To commit this change, please enter a log message,
387 your name, your email address,
388 and the correct password in the form below.
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000389 <P>
390 <HR>
391 <P>
392 """
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000393 self.showedit(name, title, text)
394 if self.add:
395 print """
396 <INPUT TYPE=hidden NAME=add VALUE=%s>
397 <INPUT TYPE=hidden NAME=section VALUE=%s>
398 <INPUT TYPE=hidden NAME=number VALUE=%s>
399 """ % (self.add, self.section, self.number)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000400 print """
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000401 <BR>
402 <INPUT TYPE=submit VALUE="Review Edit">
403 <INPUT TYPE=hidden NAME=req VALUE=review>
404 <INPUT TYPE=hidden NAME=name VALUE=%s>
Guido van Rossum74427e51997-05-21 23:43:39 +0000405 <INPUT TYPE=hidden NAME=version VALUE=%s>
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000406 </FORM>
407 <HR>
Guido van Rossum74427e51997-05-21 23:43:39 +0000408 """ % (name, self.version)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000409
Guido van Rossumf701bf11997-05-21 22:25:56 +0000410 def do_info(self):
411 name = self.name
412 headers, text = self.read(name)
413 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000414 self.error("Invalid file name", name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000415 return
Guido van Rossumc6447521997-05-23 00:50:01 +0000416 self.prologue("Info for %s" % name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000417 print '<PRE>'
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000418 p = os.popen("/depot/gnu/plat/bin/rlog -r %s </dev/null 2>&1" %
419 self.name)
420 output = p.read()
421 p.close()
422 print cgi.escape(output)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000423 print '</PRE>'
424 print '<A HREF="faq.py?req=rlog&name=%s">View full rcs log</A>' % name
425
426 def do_rlog(self):
427 name = self.name
428 headers, text = self.read(name)
429 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000430 self.error("Invalid file name", name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000431 return
Guido van Rossumc6447521997-05-23 00:50:01 +0000432 self.prologue("RCS log for %s" % name)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000433 print '<PRE>'
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000434 p = os.popen("/depot/gnu/plat/bin/rlog %s </dev/null 2>&1" % self.name)
435 output = p.read()
436 p.close()
437 print cgi.escape(output)
Guido van Rossumf701bf11997-05-21 22:25:56 +0000438 print '</PRE>'
439
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000440 def checkin(self):
441 import regsub, time, tempfile
442 name = self.name
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000443 password = self.password
444 if password != PASSWORD:
445 self.error("Invalid password.")
446 return
447 if not (self.log and self.author and '@' in self.email):
448 self.error("No log message, no author, or invalid email.")
449 return
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000450 headers, oldtext = self.read(name)
451 if not headers:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000452 self.error("Invalid file name", name)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000453 return
Guido van Rossum74427e51997-05-21 23:43:39 +0000454 version = self.version
455 curversion = self.getversion(name)
456 if version != curversion:
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000457 self.error(
458 "Version conflict.",
459 "You edited version %s but current version is %s." % (
460 version, curversion),
461 """
462 <P>
463 The two most common causes of this problem are:
464 <UL>
465 <LI>After committing a change, you went back in your browser,
466 edited the entry some more, and clicked Commit again.
467 <LI>Someone else started editing the same entry and committed
468 before you did.
469 </UL>
470 <P>
471 """,
472 '<A HREF="faq.py?req=show&name=%s"' % name,
473 '>Click here to reload the entry and try again.</A>')
Guido van Rossum74427e51997-05-21 23:43:39 +0000474 return
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000475 text = self.text
476 title = self.title
477 author = self.author
478 email = self.email
479 log = self.log
480 text = regsub.gsub("\r\n", "\n", text)
481 log = regsub.gsub("\r\n", "\n", log)
482 author = string.join(string.split(author))
483 email = string.join(string.split(email))
484 title = string.join(string.split(title))
485 oldtitle = headers['title']
486 oldtitle = string.join(string.split(oldtitle))
487 text = string.strip(text)
488 oldtext = string.strip(oldtext)
489 if text == oldtext and title == oldtitle:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000490 self.error("No changes.")
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000491 return
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000492 # Check that the FAQ entry number didn't change
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000493 if string.split(title)[:1] != string.split(oldtitle)[:1]:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000494 self.error("Don't change the FAQ entry number please.")
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000495 return
496 remhost = os.environ["REMOTE_HOST"]
497 remaddr = os.environ["REMOTE_ADDR"]
498 try:
499 os.unlink(name + "~")
500 except os.error:
501 pass
502 try:
503 os.rename(name, name + "~")
504 except os.error:
505 pass
506 try:
507 os.unlink(name)
508 except os.error:
509 pass
510 try:
511 f = open(name, "w")
512 except IOError, msg:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000513 self.error("Can't open", name, "for writing:", cgi.escape(str(msg)))
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000514 return
515 now = time.ctime(time.time())
516 f.write("Title: %s\n" % title)
517 f.write("Last-Changed-Date: %s\n" % now)
518 f.write("Last-Changed-Author: %s\n" % author)
519 f.write("Last-Changed-Email: %s\n" % email)
520 f.write("Last-Changed-Remote-Host: %s\n" % remhost)
521 f.write("Last-Changed-Remote-Address: %s\n" % remaddr)
522 keys = headers.keys()
523 keys.sort()
524 keys.remove('title')
525 for key in keys:
526 if key[:13] != 'last-changed-':
527 f.write("%s: %s\n" % (string.capwords(key, '-'),
528 headers[key]))
529 f.write("\n")
530 f.write(text)
531 f.write("\n")
532 f.close()
533
534 tfn = tempfile.mktemp()
535 f = open(tfn, "w")
536 f.write("Last-Changed-Date: %s\n" % now)
537 f.write("Last-Changed-Author: %s\n" % author)
538 f.write("Last-Changed-Email: %s\n" % email)
539 f.write("Last-Changed-Remote-Host: %s\n" % remhost)
540 f.write("Last-Changed-Remote-Address: %s\n" % remaddr)
541 f.write("\n")
542 f.write(log)
543 f.write("\n")
544 f.close()
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000545
546 # Do this for show() below
547 self.headers = {
548 'title': title,
549 'last-changed-date': now,
550 'last-changed-author': author,
551 'last-changed-email': email,
552 'last-changed-remote-host': remhost,
553 'last-changed-remote-address': remaddr,
554 }
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000555
556 p = os.popen("""
557 /depot/gnu/plat/bin/rcs -l %s </dev/null 2>&1
558 /depot/gnu/plat/bin/ci -u %s <%s 2>&1
559 rm -f %s
560 """ % (name, name, tfn, tfn))
561 output = p.read()
562 sts = p.close()
563 if not sts:
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000564 self.set_cookie(author, email, password)
Guido van Rossum74427e51997-05-21 23:43:39 +0000565 self.prologue("Python FAQ Entry Edited")
566 print "<HR>"
Guido van Rossumed531fd1997-05-22 15:21:57 +0000567 self.show(name, title, text)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000568 if output:
569 print "<PRE>%s</PRE>" % cgi.escape(output)
570 else:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000571 self.error("Python FAQ Entry Commit Failed",
572 "Exit status 0x%04x" % sts)
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000573 if output:
574 print "<PRE>%s</PRE>" % cgi.escape(output)
575
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000576 def set_cookie(self, author, email, password):
Guido van Rossum9c6ceda1997-05-23 22:44:01 +0000577 name = "Python-FAQ-Wizard"
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000578 value = "%s/%s/%s" % (author, email, password)
Guido van Rossumaf5be951997-05-22 16:57:50 +0000579 import urllib
580 value = urllib.quote(value)
581 print "Set-Cookie: %s=%s; path=/cgi-bin/;" % (name, value),
Guido van Rossum1d579811997-05-23 19:18:35 +0000582 import time
583 now = time.time()
584 then = now + 28 * 24 * 3600
585 gmt = time.gmtime(then)
586 print time.strftime("expires=%a, %d-%b-%x %X GMT", gmt)
Guido van Rossumaf5be951997-05-22 16:57:50 +0000587
588 def get_cookie(self):
589 if not os.environ.has_key('HTTP_COOKIE'):
Guido van Rossum9c6ceda1997-05-23 22:44:01 +0000590 return "", "", ""
Guido van Rossumaf5be951997-05-22 16:57:50 +0000591 raw = os.environ['HTTP_COOKIE']
Guido van Rossum9c6ceda1997-05-23 22:44:01 +0000592 words = map(string.strip, string.split(raw, ';'))
Guido van Rossumaf5be951997-05-22 16:57:50 +0000593 cookies = {}
594 for word in words:
595 i = string.find(word, '=')
596 if i >= 0:
597 key, value = word[:i], word[i+1:]
598 cookies[key] = value
Guido van Rossum9c6ceda1997-05-23 22:44:01 +0000599 print "\n<PRE>Cookies:", cookies, "</PRE>"
600 if not cookies.has_key('Python-FAQ-Wizard'):
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000601 return "", "", ""
Guido van Rossum9c6ceda1997-05-23 22:44:01 +0000602 value = cookies['Python-FAQ-Wizard']
Guido van Rossumaf5be951997-05-22 16:57:50 +0000603 import urllib
604 value = urllib.unquote(value)
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000605 words = string.split(value, '/')
606 while len(words) < 3:
607 words.append('')
608 author = string.join(words[:-2], '/')
609 email = words[-2]
610 password = words[-1]
611 return author, email, password
Guido van Rossumaf5be951997-05-22 16:57:50 +0000612
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000613 def showedit(self, name, title, text):
Guido van Rossumaf5be951997-05-22 16:57:50 +0000614 author = self.author
615 email = self.email
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000616 password = self.password
Guido van Rossum9c6ceda1997-05-23 22:44:01 +0000617 if not author or not email or not password:
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000618 a, e, p = self.get_cookie()
Guido van Rossumaf5be951997-05-22 16:57:50 +0000619 author = author or a
620 email = email or e
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000621 password = password or p
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000622 print """
Guido van Rossumed531fd1997-05-22 15:21:57 +0000623 Title: <INPUT TYPE=text SIZE=70 NAME=title VALUE="%s"><BR>
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000624 <TEXTAREA COLS=80 ROWS=20 NAME=text>%s\n</TEXTAREA>""" % (
625 self.escape(title), cgi.escape(string.strip(text)))
626 print """<BR>
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000627 Log message (reason for the change):<BR>
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000628 <TEXTAREA COLS=80 ROWS=5 NAME=log>%s\n</TEXTAREA><BR>
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000629 Please provide the following information for logging purposes:
Guido van Rossuma78a3c31997-05-23 22:29:24 +0000630 <TABLE FRAME=none COLS=2>
631 <TR>
632 <TD>Name:
633 <TD><INPUT TYPE=text SIZE=40 NAME=author VALUE="%s">
634 <TR>
635 <TD>Email:
636 <TD><INPUT TYPE=text SIZE=40 NAME=email VALUE="%s">
637 <TR>
638 <TD>Password:
639 <TD><INPUT TYPE=password SIZE=40 NAME=password VALUE="%s">
640 </TABLE>
641 """ % (self.escape(self.log), self.escape(author),
642 self.escape(email), self.escape(password))
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000643
644 def escape(self, s):
645 import regsub
646 if '&' in s:
647 s = regsub.gsub("&", "&amp;", s) # Must be done first!
648 if '<' in s:
649 s = regsub.gsub("<", "&lt;", s)
650 if '>' in s:
651 s = regsub.gsub(">", "&gt;", s)
652 if '"' in s:
653 s = regsub.gsub('"', "&quot;", s)
654 return s
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000655
656 def showheaders(self, headers):
657 print "<UL>"
658 keys = map(string.lower, headers.keys())
659 keys.sort()
660 for key in keys:
661 print "<LI><B>%s:</B> %s" % (string.capwords(key, '-'),
662 headers[key] or '')
663 print "</UL>"
664
Guido van Rossumed531fd1997-05-22 15:21:57 +0000665 headers = None
666
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000667 def read(self, name):
Guido van Rossumed531fd1997-05-22 15:21:57 +0000668 self.headers = None
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000669 import fnmatch, rfc822
670 if not fnmatch.fnmatch(name, NAMEPAT):
671 return None, None
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000672 if self.add:
673 try:
674 fname = "faq%02d.%03d.htp" % (string.atoi(self.section),
675 string.atoi(self.number))
676 except ValueError:
677 return None, None
678 if fname != name:
679 return None, None
680 headers = {'title': "%s.%s. " % (self.section, self.number)}
681 text = ""
Guido van Rossumed531fd1997-05-22 15:21:57 +0000682 else:
683 f = open(name)
684 headers = rfc822.Message(f)
685 text = f.read()
686 f.close()
687 self.headers = headers
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000688 return headers, text
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000689
Guido van Rossumed531fd1997-05-22 15:21:57 +0000690 def show(self, name, title, text, edit=1):
Guido van Rossumd7bfa801997-05-21 21:31:39 +0000691 print "<H2>%s</H2>" % cgi.escape(title)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000692 pre = 0
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000693 for line in string.split(text, '\n'):
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000694 if not string.strip(line):
695 if pre:
696 print '</PRE>'
697 pre = 0
698 else:
699 print '<P>'
700 else:
Guido van Rossum5527db51997-05-23 04:44:30 +0000701 if line[0] not in string.whitespace:
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000702 if pre:
703 print '</PRE>'
704 pre = 0
705 else:
706 if not pre:
707 print '<PRE>'
708 pre = 1
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000709 if '/' in line or '@' in line:
710 line = self.translate(line)
Guido van Rossuma0e9a6d1997-05-23 18:13:58 +0000711 elif '<' in line or '&' in line:
712 line = cgi.escape(line)
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000713 if not pre and '*' in line:
714 line = self.emphasize(line)
715 print line
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000716 if pre:
717 print '</PRE>'
718 pre = 0
719 print '<P>'
720 if edit:
Guido van Rossumf701bf11997-05-21 22:25:56 +0000721 print """
722 <A HREF="faq.py?req=edit&name=%s">Edit this entry</A> /
Guido van Rossum1d579811997-05-23 19:18:35 +0000723 <A HREF="faq.py?req=info&name=%s" TARGET=rlog>Log info</A>
Guido van Rossumf701bf11997-05-21 22:25:56 +0000724 """ % (name, name)
Guido van Rossumed531fd1997-05-22 15:21:57 +0000725 if self.headers:
726 try:
727 date = self.headers['last-changed-date']
728 author = self.headers['last-changed-author']
729 email = self.headers['last-changed-email']
730 except KeyError:
731 pass
732 else:
Guido van Rossum1d579811997-05-23 19:18:35 +0000733 s = '/ Last changed on %s by <A HREF="mailto:%s">%s</A>'
Guido van Rossumed531fd1997-05-22 15:21:57 +0000734 print s % (date, email, author)
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000735 print '<P>'
736 print "<HR>"
737
Guido van Rossum74427e51997-05-21 23:43:39 +0000738 def getversion(self, name):
739 p = os.popen("/depot/gnu/plat/bin/rlog -h %s </dev/null 2>&1" % name)
Guido van Rossum64099e91997-05-22 15:49:23 +0000740 head = "*new*"
Guido van Rossum74427e51997-05-21 23:43:39 +0000741 while 1:
742 line = p.readline()
743 if not line:
744 break
745 if line[:5] == 'head:':
746 head = string.strip(line[5:])
747 p.close()
748 return head
749
750 def prologue(self, title):
751 title = cgi.escape(title)
Guido van Rossumaf5be951997-05-22 16:57:50 +0000752 print '''
Guido van Rossum74427e51997-05-21 23:43:39 +0000753 <HTML>
754 <HEAD>
755 <TITLE>%s</TITLE>
756 </HEAD>
757 <BODY BACKGROUND="http://www.python.org/pics/RedShort.gif"
758 BGCOLOR="#FFFFFF"
759 TEXT="#000000"
760 LINK="#AA0000"
761 VLINK="#906A6A">
762 <H1>%s</H1>
763 ''' % (title, title)
764
Guido van Rossumaf5be951997-05-22 16:57:50 +0000765 def error(self, *messages):
766 self.prologue("Python FAQ error")
767 print "Sorry, an error occurred:<BR>"
768 for message in messages:
769 print message,
770 print
771
Guido van Rossum74427e51997-05-21 23:43:39 +0000772 def epilogue(self):
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000773 if self.edit == 'no':
774 global wanttime
775 wanttime = 0
776 else:
777 print '''
778 <P>
779 <HR>
780 <A HREF="http://www.python.org">Python home</A> /
Guido van Rossuma0e9a6d1997-05-23 18:13:58 +0000781 <A HREF="faq.py?req=frontpage">FAQ home</A> /
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000782 Feedback to <A HREF="mailto:guido@python.org">GvR</A>
783 '''
Guido van Rossum74427e51997-05-21 23:43:39 +0000784 print '''
Guido van Rossum74427e51997-05-21 23:43:39 +0000785 </BODY>
786 </HTML>
787 '''
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000788
Guido van Rossum5527db51997-05-23 04:44:30 +0000789 translate_prog = None
790
791 def translate(self, text):
792 if not self.translate_prog:
793 import regex
794 url = '\(http\|ftp\)://[^ \t\r\n]*'
795 email = '\<[-a-zA-Z0-9._]+@[-a-zA-Z0-9._]+'
796 self.translate_prog = prog = regex.compile(url + "\|" + email)
797 else:
798 prog = self.translate_prog
799 i = 0
800 list = []
801 while 1:
802 j = prog.search(text, i)
803 if j < 0:
804 break
805 list.append(cgi.escape(text[i:j]))
806 i = j
807 url = prog.group(0)
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000808 while url[-1] in ");:,.?'\"":
Guido van Rossum5527db51997-05-23 04:44:30 +0000809 url = url[:-1]
Guido van Rossum1dcc2441997-05-23 18:53:06 +0000810 url = self.escape(url)
Guido van Rossum5527db51997-05-23 04:44:30 +0000811 if ':' in url:
812 repl = '<A HREF="%s">%s</A>' % (url, url)
813 else:
814 repl = '<A HREF="mailto:%s">&lt;%s&gt;</A>' % (url, url)
815 list.append(repl)
816 i = i + len(url)
817 j = len(text)
818 list.append(cgi.escape(text[i:j]))
819 return string.join(list, '')
820
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000821 emphasize_prog = None
822
823 def emphasize(self, line):
824 import regsub
825 if not self.emphasize_prog:
826 import regex
827 pat = "\*\([a-zA-Z]+\)\*"
828 self.emphasize_prog = prog = regex.compile(pat)
829 else:
830 prog = self.emphasize_prog
831 return regsub.gsub(prog, "<I>\\1</I>", line)
832
Guido van Rossumaf5be951997-05-22 16:57:50 +0000833print "Content-type: text/html"
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000834dt = 0
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000835wanttime = 0
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000836try:
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000837 import time
838 t1 = time.time()
Guido van Rossum74427e51997-05-21 23:43:39 +0000839 import cgi, string, os, sys
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000840 x = FAQServer()
841 x.main()
Guido van Rossum3c3354c1997-05-21 16:52:18 +0000842 t2 = time.time()
843 dt = t2-t1
Guido van Rossumf8f0fb71997-05-23 21:21:43 +0000844 wanttime = 1
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000845except:
Guido van Rossumaf5be951997-05-22 16:57:50 +0000846 print "\n<HR>Sorry, an error occurred"
Guido van Rossumadb3a9d1997-05-21 07:24:50 +0000847 cgi.print_exception()
Guido van Rossumd1c1ec81997-05-23 17:45:04 +0000848if wanttime:
849 print "<BR>(running time = %s seconds)" % str(round(dt, 3))
Guido van Rossumed531fd1997-05-22 15:21:57 +0000850
851# The following bootstrap script must be placed in cgi-bin/faq.py:
852BOOTSTRAP = """
853#! /usr/local/bin/python
854FAQDIR = "/usr/people/guido/python/FAQ"
855import os, sys
856os.chdir(FAQDIR)
857sys.path.insert(0, os.curdir)
858import faqmain
859"""