blob: 804e6116bd6e5c56bfd47089703cf94dbba7ad18 [file] [log] [blame]
Guido van Rossumea31ea21997-05-26 05:43:29 +00001"""Generic FAQ Wizard.
Guido van Rossum1677e5b1997-05-26 00:07:18 +00002
Guido van Rossumea31ea21997-05-26 05:43:29 +00003This is a CGI program that maintains a user-editable FAQ. It uses RCS
4to keep track of changes to individual FAQ entries. It is fully
5configurable; everything you might want to change when using this
6program to maintain some other FAQ than the Python FAQ is contained in
7the configuration module, faqconf.py.
8
9Note that this is not an executable script; it's an importable module.
10The actual script in cgi-bin minimal; it's appended at the end of this
11file as a string literal.
12
13"""
14
15import sys, string, time, os, stat, regex, cgi, faqconf
16from faqconf import * # This imports all uppercase names
Guido van Rossum7a241071997-05-26 19:46:56 +000017now = time.time()
Guido van Rossum1677e5b1997-05-26 00:07:18 +000018
19class FileError:
20 def __init__(self, file):
21 self.file = file
22
23class InvalidFile(FileError):
24 pass
25
Guido van Rossumea31ea21997-05-26 05:43:29 +000026class NoSuchSection(FileError):
27 def __init__(self, section):
28 FileError.__init__(self, NEWFILENAME %(section, 1))
29 self.section = section
30
Guido van Rossum1677e5b1997-05-26 00:07:18 +000031class NoSuchFile(FileError):
32 def __init__(self, file, why=None):
33 FileError.__init__(self, file)
34 self.why = why
35
Guido van Rossumea31ea21997-05-26 05:43:29 +000036def replace(s, old, new):
37 try:
38 return string.replace(s, old, new)
39 except AttributeError:
40 return string.join(string.split(s, old), new)
41
42def escape(s):
43 s = replace(s, '&', '&')
44 s = replace(s, '<', '&lt;')
45 s = replace(s, '>', '&gt')
46 return s
47
Guido van Rossum1677e5b1997-05-26 00:07:18 +000048def escapeq(s):
49 s = escape(s)
Guido van Rossumea31ea21997-05-26 05:43:29 +000050 s = replace(s, '"', '&quot;')
Guido van Rossum1677e5b1997-05-26 00:07:18 +000051 return s
52
Guido van Rossumea31ea21997-05-26 05:43:29 +000053def _interpolate(format, args, kw):
54 try:
55 quote = kw['_quote']
56 except KeyError:
57 quote = 1
58 d = (kw,) + args + (faqconf.__dict__,)
59 m = MagicDict(d, quote)
60 return format % m
Guido van Rossum1677e5b1997-05-26 00:07:18 +000061
Guido van Rossumea31ea21997-05-26 05:43:29 +000062def interpolate(format, *args, **kw):
63 return _interpolate(format, args, kw)
64
65def emit(format, *args, **kw):
66 try:
67 f = kw['_file']
68 except KeyError:
69 f = sys.stdout
70 f.write(_interpolate(format, args, kw))
Guido van Rossum1677e5b1997-05-26 00:07:18 +000071
72translate_prog = None
73
74def translate(text):
75 global translate_prog
76 if not translate_prog:
Guido van Rossum1677e5b1997-05-26 00:07:18 +000077 url = '\(http\|ftp\)://[^ \t\r\n]*'
78 email = '\<[-a-zA-Z0-9._]+@[-a-zA-Z0-9._]+'
Guido van Rossumea31ea21997-05-26 05:43:29 +000079 translate_prog = prog = regex.compile(url + '\|' + email)
Guido van Rossum1677e5b1997-05-26 00:07:18 +000080 else:
81 prog = translate_prog
82 i = 0
83 list = []
84 while 1:
85 j = prog.search(text, i)
86 if j < 0:
87 break
Guido van Rossumea31ea21997-05-26 05:43:29 +000088 list.append(escape(text[i:j]))
Guido van Rossum1677e5b1997-05-26 00:07:18 +000089 i = j
90 url = prog.group(0)
Guido van Rossumea31ea21997-05-26 05:43:29 +000091 while url[-1] in ');:,.?\'"':
Guido van Rossum1677e5b1997-05-26 00:07:18 +000092 url = url[:-1]
93 url = escape(url)
94 if ':' in url:
95 repl = '<A HREF="%s">%s</A>' % (url, url)
96 else:
97 repl = '<A HREF="mailto:%s">&lt;%s&gt;</A>' % (url, url)
98 list.append(repl)
99 i = i + len(url)
100 j = len(text)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000101 list.append(escape(text[i:j]))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000102 return string.join(list, '')
103
104emphasize_prog = None
105
106def emphasize(line):
107 global emphasize_prog
108 import regsub
109 if not emphasize_prog:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000110 pat = '\*\([a-zA-Z]+\)\*'
111 emphasize_prog = regex.compile(pat)
112 return regsub.gsub(emphasize_prog, '<I>\\1</I>', line)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000113
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000114revparse_prog = None
115
116def revparse(rev):
117 global revparse_prog
118 if not revparse_prog:
119 revparse_prog = regex.compile(
120 '^\([1-9][0-9]?[0-9]?\)\.\([1-9][0-9]?[0-9]?[0-9]?\)$')
121 if revparse_prog.match(rev) < 0:
122 return None
123 [major, minor] = map(string.atoi, revparse_prog.group(1, 2))
124 return major, minor
125
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000126def load_cookies():
127 if not os.environ.has_key('HTTP_COOKIE'):
128 return {}
129 raw = os.environ['HTTP_COOKIE']
130 words = map(string.strip, string.split(raw, ';'))
131 cookies = {}
132 for word in words:
133 i = string.find(word, '=')
134 if i >= 0:
135 key, value = word[:i], word[i+1:]
136 cookies[key] = value
137 return cookies
138
139def load_my_cookie():
140 cookies = load_cookies()
141 try:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000142 value = cookies[COOKIE_NAME]
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000143 except KeyError:
144 return {}
145 import urllib
146 value = urllib.unquote(value)
147 words = string.split(value, '/')
148 while len(words) < 3:
149 words.append('')
150 author = string.join(words[:-2], '/')
151 email = words[-2]
152 password = words[-1]
153 return {'author': author,
154 'email': email,
155 'password': password}
156
Guido van Rossumea31ea21997-05-26 05:43:29 +0000157def send_my_cookie(ui):
158 name = COOKIE_NAME
159 value = "%s/%s/%s" % (ui.author, ui.email, ui.password)
160 import urllib
161 value = urllib.quote(value)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000162 then = now + COOKIE_LIFETIME
163 gmt = time.gmtime(then)
164 print "Set-Cookie: %s=%s; path=/cgi-bin/;" % (name, value),
165 print time.strftime("expires=%a, %d-%b-%x %X GMT", gmt)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000166
Guido van Rossumea31ea21997-05-26 05:43:29 +0000167class MagicDict:
168
169 def __init__(self, d, quote):
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000170 self.__d = d
Guido van Rossumea31ea21997-05-26 05:43:29 +0000171 self.__quote = quote
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000172
173 def __getitem__(self, key):
174 for d in self.__d:
175 try:
176 value = d[key]
177 if value:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000178 value = str(value)
179 if self.__quote:
180 value = escapeq(value)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000181 return value
182 except KeyError:
183 pass
Guido van Rossumea31ea21997-05-26 05:43:29 +0000184 return ''
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000185
186class UserInput:
187
188 def __init__(self):
189 self.__form = cgi.FieldStorage()
190
191 def __getattr__(self, name):
192 if name[0] == '_':
193 raise AttributeError
194 try:
195 value = self.__form[name].value
196 except (TypeError, KeyError):
197 value = ''
198 else:
199 value = string.strip(value)
200 setattr(self, name, value)
201 return value
202
203 def __getitem__(self, key):
204 return getattr(self, key)
205
Guido van Rossumea31ea21997-05-26 05:43:29 +0000206class FaqEntry:
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000207
Guido van Rossumea31ea21997-05-26 05:43:29 +0000208 def __init__(self, fp, file, sec_num):
209 self.file = file
210 self.sec, self.num = sec_num
211 if fp:
212 import rfc822
213 self.__headers = rfc822.Message(fp)
214 self.body = string.strip(fp.read())
215 else:
216 self.__headers = {'title': "%d.%d. " % sec_num}
217 self.body = ''
218
219 def __getattr__(self, name):
220 if name[0] == '_':
221 raise AttributeError
222 key = string.join(string.split(name, '_'), '-')
223 try:
224 value = self.__headers[key]
225 except KeyError:
226 value = ''
227 setattr(self, name, value)
228 return value
229
230 def __getitem__(self, key):
231 return getattr(self, key)
232
233 def load_version(self):
234 command = interpolate(SH_RLOG_H, self)
235 p = os.popen(command)
236 version = ''
237 while 1:
238 line = p.readline()
239 if not line:
240 break
241 if line[:5] == 'head:':
242 version = string.strip(line[5:])
243 p.close()
244 self.version = version
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000245
Guido van Rossum7a241071997-05-26 19:46:56 +0000246 def getmtime(self):
247 if not self.last_changed_date:
248 return 0
249 try:
250 return os.stat(self.file)[stat.ST_MTIME]
251 except os.error:
252 return 0
253
254 def emit_marks(self):
255 mtime = self.getmtime()
256 if mtime >= now - DT_VERY_RECENT:
257 emit(MARK_VERY_RECENT, self)
258 elif mtime >= now - DT_RECENT:
259 emit(MARK_RECENT, self)
260
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000261 def show(self, edit=1):
Guido van Rossum7a241071997-05-26 19:46:56 +0000262 emit(ENTRY_HEADER1, self)
263 self.emit_marks()
264 emit(ENTRY_HEADER2, self)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000265 pre = 0
Guido van Rossumea31ea21997-05-26 05:43:29 +0000266 for line in string.split(self.body, '\n'):
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000267 if not string.strip(line):
268 if pre:
269 print '</PRE>'
270 pre = 0
271 else:
272 print '<P>'
273 else:
274 if line[0] not in string.whitespace:
275 if pre:
276 print '</PRE>'
277 pre = 0
278 else:
279 if not pre:
280 print '<PRE>'
281 pre = 1
282 if '/' in line or '@' in line:
283 line = translate(line)
284 elif '<' in line or '&' in line:
285 line = escape(line)
286 if not pre and '*' in line:
287 line = emphasize(line)
288 print line
289 if pre:
290 print '</PRE>'
291 pre = 0
292 if edit:
293 print '<P>'
Guido van Rossumea31ea21997-05-26 05:43:29 +0000294 emit(ENTRY_FOOTER, self)
295 if self.last_changed_date:
296 emit(ENTRY_LOGINFO, self)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000297 print '<P>'
298
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000299class FaqDir:
300
301 entryclass = FaqEntry
302
Guido van Rossumea31ea21997-05-26 05:43:29 +0000303 __okprog = regex.compile(OKFILENAME)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000304
305 def __init__(self, dir=os.curdir):
306 self.__dir = dir
307 self.__files = None
308
309 def __fill(self):
310 if self.__files is not None:
311 return
312 self.__files = files = []
313 okprog = self.__okprog
314 for file in os.listdir(self.__dir):
315 if okprog.match(file) >= 0:
316 files.append(file)
317 files.sort()
318
319 def good(self, file):
320 return self.__okprog.match(file) >= 0
321
322 def parse(self, file):
323 if not self.good(file):
324 return None
325 sec, num = self.__okprog.group(1, 2)
326 return string.atoi(sec), string.atoi(num)
327
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000328 def list(self):
329 # XXX Caller shouldn't modify result
330 self.__fill()
331 return self.__files
332
333 def open(self, file):
334 sec_num = self.parse(file)
335 if not sec_num:
336 raise InvalidFile(file)
337 try:
338 fp = open(file)
339 except IOError, msg:
340 raise NoSuchFile(file, msg)
341 try:
342 return self.entryclass(fp, file, sec_num)
343 finally:
344 fp.close()
345
346 def show(self, file, edit=1):
347 self.open(file).show(edit=edit)
348
Guido van Rossumea31ea21997-05-26 05:43:29 +0000349 def new(self, section):
350 if not SECTION_TITLES.has_key(section):
351 raise NoSuchSection(section)
352 maxnum = 0
353 for file in self.list():
354 sec, num = self.parse(file)
355 if sec == section:
356 maxnum = max(maxnum, num)
357 sec_num = (section, maxnum+1)
358 file = NEWFILENAME % sec_num
359 return self.entryclass(None, file, sec_num)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000360
361class FaqWizard:
362
363 def __init__(self):
364 self.ui = UserInput()
365 self.dir = FaqDir()
366
367 def go(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000368 print 'Content-type: text/html'
369 req = self.ui.req or 'home'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000370 mname = 'do_%s' % req
371 try:
372 meth = getattr(self, mname)
373 except AttributeError:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000374 self.error("Bad request type %s." % `req`)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000375 else:
376 try:
377 meth()
378 except InvalidFile, exc:
379 self.error("Invalid entry file name %s" % exc.file)
380 except NoSuchFile, exc:
381 self.error("No entry with file name %s" % exc.file)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000382 except NoSuchSection, exc:
383 self.error("No section number %s" % exc.section)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000384 self.epilogue()
385
386 def error(self, message, **kw):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000387 self.prologue(T_ERROR)
388 emit(message, kw)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000389
390 def prologue(self, title, entry=None, **kw):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000391 emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000392
393 def epilogue(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000394 emit(EPILOGUE)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000395
396 def do_home(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000397 self.prologue(T_HOME)
398 emit(HOME)
399
400 def do_debug(self):
401 self.prologue("FAQ Wizard Debugging")
402 form = cgi.FieldStorage()
403 cgi.print_form(form)
404 cgi.print_environ(os.environ)
405 cgi.print_directory()
406 cgi.print_arguments()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000407
408 def do_search(self):
409 query = self.ui.query
410 if not query:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000411 self.error("Empty query string!")
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000412 return
Guido van Rossum8cde0b41997-05-26 16:35:46 +0000413 if self.ui.querytype == 'simple':
Guido van Rossumea31ea21997-05-26 05:43:29 +0000414 for c in '\\.[]?+^$*':
415 if c in query:
416 query = replace(query, c, '\\'+c)
Guido van Rossum8cde0b41997-05-26 16:35:46 +0000417 queries = [query]
418 elif self.ui.querytype in ('anykeywords', 'allkeywords'):
419 import regsub
420 words = string.split(regsub.gsub('[^a-zA-Z0-9]+', ' ', query))
421 if not words:
422 self.error("No keywords specified!")
423 return
424 words = map(lambda w: '\<%s\>' % w, words)
425 if self.ui.querytype[:3] == 'any':
426 queries = [string.join(words, '\|')]
427 else:
428 queries = words
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000429 else:
Guido van Rossum8cde0b41997-05-26 16:35:46 +0000430 # Default to regex
431 queries = [query]
432 self.prologue(T_SEARCH)
433 progs = []
434 for query in queries:
435 if self.ui.casefold == 'no':
436 p = regex.compile(query)
437 else:
438 p = regex.compile(query, regex.casefold)
439 progs.append(p)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000440 hits = []
441 for file in self.dir.list():
442 try:
443 entry = self.dir.open(file)
444 except FileError:
445 constants
Guido van Rossum8cde0b41997-05-26 16:35:46 +0000446 for p in progs:
447 if p.search(entry.title) < 0 and p.search(entry.body) < 0:
448 break
449 else:
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000450 hits.append(file)
451 if not hits:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000452 emit(NO_HITS, self.ui, count=0)
453 elif len(hits) <= MAXHITS:
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000454 if len(hits) == 1:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000455 emit(ONE_HIT, count=1)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000456 else:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000457 emit(FEW_HITS, count=len(hits))
Guido van Rossum21c4b5f1997-05-26 06:28:40 +0000458 self.format_all(hits, headers=0)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000459 else:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000460 emit(MANY_HITS, count=len(hits))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000461 self.format_index(hits)
462
463 def do_all(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000464 self.prologue(T_ALL)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000465 files = self.dir.list()
466 self.last_changed(files)
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000467 self.format_index(files, localrefs=1)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000468 self.format_all(files)
469
470 def do_compat(self):
471 files = self.dir.list()
Guido van Rossumea31ea21997-05-26 05:43:29 +0000472 emit(COMPAT)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000473 self.last_changed(files)
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000474 self.format_index(files, localrefs=1)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000475 self.format_all(files, edit=0)
Guido van Rossum7a241071997-05-26 19:46:56 +0000476 sys.exit(0) # XXX Hack to suppress epilogue
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000477
478 def last_changed(self, files):
479 latest = 0
480 for file in files:
Guido van Rossum7a241071997-05-26 19:46:56 +0000481 entry = self.dir.open(file)
482 if entry:
483 mtime = mtime = entry.getmtime()
484 if mtime > latest:
485 latest = mtime
Guido van Rossumc22eb011997-06-02 15:51:51 +0000486 print time.strftime(LAST_CHANGED, time.localtime(latest))
Guido van Rossum7a241071997-05-26 19:46:56 +0000487 emit(EXPLAIN_MARKS)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000488
Guido van Rossum21c4b5f1997-05-26 06:28:40 +0000489 def format_all(self, files, edit=1, headers=1):
490 sec = 0
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000491 for file in files:
Guido van Rossum21c4b5f1997-05-26 06:28:40 +0000492 try:
493 entry = self.dir.open(file)
494 except NoSuchFile:
495 continue
496 if headers and entry.sec != sec:
497 sec = entry.sec
498 try:
499 title = SECTION_TITLES[sec]
500 except KeyError:
501 title = "Untitled"
502 emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n",
503 sec=sec, title=title)
504 entry.show(edit=edit)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000505
506 def do_index(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000507 self.prologue(T_INDEX)
Guido van Rossum7a241071997-05-26 19:46:56 +0000508 files = self.dir.list()
509 self.last_changed(files)
510 self.format_index(files, add=1)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000511
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000512 def format_index(self, files, add=0, localrefs=0):
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000513 sec = 0
514 for file in files:
515 try:
516 entry = self.dir.open(file)
517 except NoSuchFile:
518 continue
519 if entry.sec != sec:
520 if sec:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000521 if add:
522 emit(INDEX_ADDSECTION, sec=sec)
523 emit(INDEX_ENDSECTION, sec=sec)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000524 sec = entry.sec
Guido van Rossum21c4b5f1997-05-26 06:28:40 +0000525 try:
526 title = SECTION_TITLES[sec]
527 except KeyError:
528 title = "Untitled"
529 emit(INDEX_SECTION, sec=sec, title=title)
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000530 if localrefs:
531 emit(LOCAL_ENTRY, entry)
532 else:
533 emit(INDEX_ENTRY, entry)
Guido van Rossum7a241071997-05-26 19:46:56 +0000534 entry.emit_marks()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000535 if sec:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000536 if add:
537 emit(INDEX_ADDSECTION, sec=sec)
538 emit(INDEX_ENDSECTION, sec=sec)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000539
540 def do_recent(self):
541 if not self.ui.days:
542 days = 1
543 else:
544 days = string.atof(self.ui.days)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000545 try:
546 cutoff = now - days * 24 * 3600
547 except OverflowError:
548 cutoff = 0
549 list = []
550 for file in self.dir.list():
Guido van Rossum7a241071997-05-26 19:46:56 +0000551 entry = self.dir.open(file)
552 if not entry:
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000553 continue
Guido van Rossum7a241071997-05-26 19:46:56 +0000554 mtime = entry.getmtime()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000555 if mtime >= cutoff:
556 list.append((mtime, file))
557 list.sort()
558 list.reverse()
Guido van Rossumea31ea21997-05-26 05:43:29 +0000559 self.prologue(T_RECENT)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000560 if days <= 1:
561 period = "%.2g hours" % (days*24)
562 else:
563 period = "%.6g days" % days
564 if not list:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000565 emit(NO_RECENT, period=period)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000566 elif len(list) == 1:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000567 emit(ONE_RECENT, period=period)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000568 else:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000569 emit(SOME_RECENT, period=period, count=len(list))
Guido van Rossum1f047721997-05-26 16:02:00 +0000570 self.format_all(map(lambda (mtime, file): file, list), headers=0)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000571 emit(TAIL_RECENT)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000572
573 def do_roulette(self):
Guido van Rossum525d52f1997-06-02 22:52:37 +0000574 import whrandom
575 files = self.dir.list()
576 if not files:
Guido van Rossum72a342f1997-05-30 11:58:21 +0000577 self.error("No entries.")
578 return
Guido van Rossum525d52f1997-06-02 22:52:37 +0000579 file = whrandom.choice(files)
Guido van Rossum72a342f1997-05-30 11:58:21 +0000580 self.prologue(T_ROULETTE)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000581 self.dir.show(file)
582
583 def do_help(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000584 self.prologue(T_HELP)
585 emit(HELP)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000586
587 def do_show(self):
588 entry = self.dir.open(self.ui.file)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000589 self.prologue(T_SHOW)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000590 entry.show()
591
592 def do_add(self):
593 self.prologue(T_ADD)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000594 emit(ADD_HEAD)
595 sections = SECTION_TITLES.items()
596 sections.sort()
597 for section, title in sections:
598 emit(ADD_SECTION, section=section, title=title)
599 emit(ADD_TAIL)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000600
601 def do_delete(self):
602 self.prologue(T_DELETE)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000603 emit(DELETE)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000604
605 def do_log(self):
606 entry = self.dir.open(self.ui.file)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000607 self.prologue(T_LOG, entry)
608 emit(LOG, entry)
609 self.rlog(interpolate(SH_RLOG, entry), entry)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000610
611 def rlog(self, command, entry=None):
612 output = os.popen(command).read()
Guido van Rossumea31ea21997-05-26 05:43:29 +0000613 sys.stdout.write('<PRE>')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000614 athead = 0
Guido van Rossumea31ea21997-05-26 05:43:29 +0000615 lines = string.split(output, '\n')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000616 while lines and not lines[-1]:
617 del lines[-1]
618 if lines:
619 line = lines[-1]
620 if line[:1] == '=' and len(line) >= 40 and \
621 line == line[0]*len(line):
622 del lines[-1]
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000623 headrev = None
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000624 for line in lines:
625 if entry and athead and line[:9] == 'revision ':
626 rev = string.strip(line[9:])
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000627 mami = revparse(rev)
628 if not mami:
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000629 print line
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000630 else:
631 emit(REVISIONLINK, entry, rev=rev, line=line)
632 if mami[1] > 1:
633 prev = "%d.%d" % (mami[0], mami[1]-1)
634 emit(DIFFLINK, entry, prev=prev, rev=rev)
635 if headrev:
636 emit(DIFFLINK, entry, prev=rev, rev=headrev)
637 else:
638 headrev = rev
639 print
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000640 athead = 0
641 else:
642 athead = 0
643 if line[:1] == '-' and len(line) >= 20 and \
644 line == len(line) * line[0]:
645 athead = 1
Guido van Rossumea31ea21997-05-26 05:43:29 +0000646 sys.stdout.write('<HR>')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000647 else:
648 print line
Guido van Rossumea31ea21997-05-26 05:43:29 +0000649 print '</PRE>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000650
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000651 def do_revision(self):
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000652 entry = self.dir.open(self.ui.file)
653 rev = self.ui.rev
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000654 mami = revparse(rev)
655 if not mami:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000656 self.error("Invalid revision number: %s." % `rev`)
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000657 self.prologue(T_REVISION, entry)
658 self.shell(interpolate(SH_REVISION, entry, rev=rev))
659
660 def do_diff(self):
661 entry = self.dir.open(self.ui.file)
662 prev = self.ui.prev
663 rev = self.ui.rev
664 mami = revparse(rev)
665 if not mami:
666 self.error("Invalid revision number: %s." % `rev`)
667 if prev:
668 if not revparse(prev):
669 self.error("Invalid previous revision number: %s." % `prev`)
670 else:
671 prev = '%d.%d' % (mami[0], mami[1])
Guido van Rossumea31ea21997-05-26 05:43:29 +0000672 self.prologue(T_DIFF, entry)
673 self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000674
675 def shell(self, command):
676 output = os.popen(command).read()
Guido van Rossumea31ea21997-05-26 05:43:29 +0000677 sys.stdout.write('<PRE>')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000678 print escape(output)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000679 print '</PRE>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000680
681 def do_new(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000682 entry = self.dir.new(section=string.atoi(self.ui.section))
683 entry.version = '*new*'
684 self.prologue(T_EDIT)
685 emit(EDITHEAD)
686 emit(EDITFORM1, entry, editversion=entry.version)
687 emit(EDITFORM2, entry, load_my_cookie())
688 emit(EDITFORM3)
689 entry.show(edit=0)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000690
691 def do_edit(self):
692 entry = self.dir.open(self.ui.file)
693 entry.load_version()
Guido van Rossumea31ea21997-05-26 05:43:29 +0000694 self.prologue(T_EDIT)
695 emit(EDITHEAD)
696 emit(EDITFORM1, entry, editversion=entry.version)
697 emit(EDITFORM2, entry, load_my_cookie())
698 emit(EDITFORM3)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000699 entry.show(edit=0)
700
701 def do_review(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000702 send_my_cookie(self.ui)
703 if self.ui.editversion == '*new*':
704 sec, num = self.dir.parse(self.ui.file)
705 entry = self.dir.new(section=sec)
706 entry.version = "*new*"
707 if entry.file != self.ui.file:
708 self.error("Commit version conflict!")
709 emit(NEWCONFLICT, self.ui, sec=sec, num=num)
710 return
711 else:
712 entry = self.dir.open(self.ui.file)
713 entry.load_version()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000714 # Check that the FAQ entry number didn't change
715 if string.split(self.ui.title)[:1] != string.split(entry.title)[:1]:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000716 self.error("Don't change the entry number please!")
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000717 return
718 # Check that the edited version is the current version
719 if entry.version != self.ui.editversion:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000720 self.error("Commit version conflict!")
721 emit(VERSIONCONFLICT, entry, self.ui)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000722 return
Guido van Rossumea31ea21997-05-26 05:43:29 +0000723 commit_ok = ((not PASSWORD
724 or self.ui.password == PASSWORD)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000725 and self.ui.author
726 and '@' in self.ui.email
727 and self.ui.log)
728 if self.ui.commit:
729 if not commit_ok:
730 self.cantcommit()
731 else:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000732 self.commit(entry)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000733 return
Guido van Rossumea31ea21997-05-26 05:43:29 +0000734 self.prologue(T_REVIEW)
735 emit(REVIEWHEAD)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000736 entry.body = self.ui.body
737 entry.title = self.ui.title
738 entry.show(edit=0)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000739 emit(EDITFORM1, self.ui, entry)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000740 if commit_ok:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000741 emit(COMMIT)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000742 else:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000743 emit(NOCOMMIT)
744 emit(EDITFORM2, self.ui, entry, load_my_cookie())
745 emit(EDITFORM3)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000746
747 def cantcommit(self):
Guido van Rossumea31ea21997-05-26 05:43:29 +0000748 self.prologue(T_CANTCOMMIT)
749 print CANTCOMMIT_HEAD
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000750 if not self.ui.passwd:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000751 emit(NEED_PASSWD)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000752 if not self.ui.log:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000753 emit(NEED_LOG)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000754 if not self.ui.author:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000755 emit(NEED_AUTHOR)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000756 if not self.ui.email:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000757 emit(NEED_EMAIL)
758 print CANTCOMMIT_TAIL
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000759
Guido van Rossumea31ea21997-05-26 05:43:29 +0000760 def commit(self, entry):
761 file = entry.file
762 # Normalize line endings in body
763 if '\r' in self.ui.body:
764 import regsub
765 self.ui.body = regsub.gsub('\r\n?', '\n', self.ui.body)
766 # Normalize whitespace in title
767 self.ui.title = string.join(string.split(self.ui.title))
768 # Check that there were any changes
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000769 if self.ui.body == entry.body and self.ui.title == entry.title:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000770 self.error("You didn't make any changes!")
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000771 return
772 # XXX Should lock here
773 try:
774 os.unlink(file)
775 except os.error:
776 pass
777 try:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000778 f = open(file, 'w')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000779 except IOError, why:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000780 self.error(CANTWRITE, file=file, why=why)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000781 return
Guido van Rossum7a241071997-05-26 19:46:56 +0000782 date = time.ctime(now)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000783 emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0)
784 f.write('\n')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000785 f.write(self.ui.body)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000786 f.write('\n')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000787 f.close()
788
789 import tempfile
790 tfn = tempfile.mktemp()
Guido van Rossumea31ea21997-05-26 05:43:29 +0000791 f = open(tfn, 'w')
792 emit(LOGHEADER, self.ui, os.environ, date=date, _file=f)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000793 f.close()
794
795 command = interpolate(
Guido van Rossumea31ea21997-05-26 05:43:29 +0000796 SH_LOCK + '\n' + SH_CHECKIN,
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000797 file=file, tfn=tfn)
798
799 p = os.popen(command)
800 output = p.read()
801 sts = p.close()
802 # XXX Should unlock here
803 if not sts:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000804 self.prologue(T_COMMITTED)
805 emit(COMMITTED)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000806 else:
Guido van Rossumea31ea21997-05-26 05:43:29 +0000807 self.error(T_COMMITFAILED)
808 emit(COMMITFAILED, sts=sts)
809 print '<PRE>%s</PRE>' % escape(output)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000810
811 try:
812 os.unlink(tfn)
813 except os.error:
814 pass
815
816 entry = self.dir.open(file)
817 entry.show()
818
819wiz = FaqWizard()
820wiz.go()
821
Guido van Rossumea31ea21997-05-26 05:43:29 +0000822# This bootstrap script should be placed in your cgi-bin directory.
823# You only need to edit the first two lines: change
824# /usr/local/bin/python to where your Python interpreter lives change
825# the value for FAQDIR to where your FAQ lives. The faqwiz.py and
826# faqconf.py files should live there, too.
827
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000828BOOTSTRAP = """\
829#! /usr/local/bin/python
830FAQDIR = "/usr/people/guido/python/FAQ"
Guido van Rossumea31ea21997-05-26 05:43:29 +0000831import sys, os
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000832os.chdir(FAQDIR)
833sys.path.insert(0, FAQDIR)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000834import faqwiz
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000835"""