blob: 6e45c6e51d8a52ad443d19d787aea307283934ad [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.
Guido van Rossumf1ead1a1997-08-28 02:38:01 +000010The actual script to place in cgi-bin is faqw.py.
Guido van Rossumea31ea21997-05-26 05:43:29 +000011
12"""
13
Guido van Rossum80e57fb1997-12-21 07:05:32 +000014import sys, string, time, os, stat, re, cgi, faqconf
Guido van Rossum72dc60c1998-04-06 14:24:36 +000015from faqconf import * # This imports all uppercase names
Guido van Rossum7a241071997-05-26 19:46:56 +000016now = time.time()
Guido van Rossum1677e5b1997-05-26 00:07:18 +000017
18class FileError:
19 def __init__(self, file):
Guido van Rossum72dc60c1998-04-06 14:24:36 +000020 self.file = file
Guido van Rossum1677e5b1997-05-26 00:07:18 +000021
22class InvalidFile(FileError):
23 pass
24
Guido van Rossumea31ea21997-05-26 05:43:29 +000025class NoSuchSection(FileError):
26 def __init__(self, section):
Guido van Rossum72dc60c1998-04-06 14:24:36 +000027 FileError.__init__(self, NEWFILENAME %(section, 1))
28 self.section = section
Guido van Rossumea31ea21997-05-26 05:43:29 +000029
Guido van Rossum1677e5b1997-05-26 00:07:18 +000030class NoSuchFile(FileError):
31 def __init__(self, file, why=None):
Guido van Rossum72dc60c1998-04-06 14:24:36 +000032 FileError.__init__(self, file)
33 self.why = why
Guido van Rossum1677e5b1997-05-26 00:07:18 +000034
Guido van Rossumea31ea21997-05-26 05:43:29 +000035def escape(s):
Guido van Rossum80e57fb1997-12-21 07:05:32 +000036 s = string.replace(s, '&', '&')
37 s = string.replace(s, '<', '&lt;')
38 s = string.replace(s, '>', '&gt;')
Guido van Rossumea31ea21997-05-26 05:43:29 +000039 return s
40
Guido van Rossum1677e5b1997-05-26 00:07:18 +000041def escapeq(s):
42 s = escape(s)
Guido van Rossum80e57fb1997-12-21 07:05:32 +000043 s = string.replace(s, '"', '&quot;')
Guido van Rossum1677e5b1997-05-26 00:07:18 +000044 return s
45
Guido van Rossumea31ea21997-05-26 05:43:29 +000046def _interpolate(format, args, kw):
47 try:
Guido van Rossum72dc60c1998-04-06 14:24:36 +000048 quote = kw['_quote']
Guido van Rossumea31ea21997-05-26 05:43:29 +000049 except KeyError:
Guido van Rossum72dc60c1998-04-06 14:24:36 +000050 quote = 1
Guido van Rossumea31ea21997-05-26 05:43:29 +000051 d = (kw,) + args + (faqconf.__dict__,)
52 m = MagicDict(d, quote)
53 return format % m
Guido van Rossum1677e5b1997-05-26 00:07:18 +000054
Guido van Rossumea31ea21997-05-26 05:43:29 +000055def interpolate(format, *args, **kw):
56 return _interpolate(format, args, kw)
57
58def emit(format, *args, **kw):
59 try:
Guido van Rossum72dc60c1998-04-06 14:24:36 +000060 f = kw['_file']
Guido van Rossumea31ea21997-05-26 05:43:29 +000061 except KeyError:
Guido van Rossum72dc60c1998-04-06 14:24:36 +000062 f = sys.stdout
Guido van Rossumea31ea21997-05-26 05:43:29 +000063 f.write(_interpolate(format, args, kw))
Guido van Rossum1677e5b1997-05-26 00:07:18 +000064
65translate_prog = None
66
Guido van Rossumb1823ad1997-12-09 16:04:46 +000067def translate(text, pre=0):
Guido van Rossum1677e5b1997-05-26 00:07:18 +000068 global translate_prog
69 if not translate_prog:
Guido van Rossum72dc60c1998-04-06 14:24:36 +000070 translate_prog = prog = re.compile(
71 r'\b(http|ftp|https)://\S+(\b|/)|\b[-.\w]+@[-.\w]+')
Guido van Rossum1677e5b1997-05-26 00:07:18 +000072 else:
Guido van Rossum72dc60c1998-04-06 14:24:36 +000073 prog = translate_prog
Guido van Rossum1677e5b1997-05-26 00:07:18 +000074 i = 0
75 list = []
76 while 1:
Guido van Rossum72dc60c1998-04-06 14:24:36 +000077 m = prog.search(text, i)
78 if not m:
79 break
80 j = m.start()
81 list.append(escape(text[i:j]))
82 i = j
83 url = m.group(0)
84 while url[-1] in '();:,.?\'"<>':
85 url = url[:-1]
86 i = i + len(url)
87 url = escape(url)
88 if not pre or (pre and PROCESS_PREFORMAT):
89 if ':' in url:
90 repl = '<A HREF="%s">%s</A>' % (url, url)
91 else:
Guido van Rossum0922a561998-07-07 22:39:21 +000092 repl = '<A HREF="mailto:%s">%s</A>' % (url, url)
Guido van Rossum72dc60c1998-04-06 14:24:36 +000093 else:
94 repl = url
95 list.append(repl)
Guido van Rossum1677e5b1997-05-26 00:07:18 +000096 j = len(text)
Guido van Rossumea31ea21997-05-26 05:43:29 +000097 list.append(escape(text[i:j]))
Guido van Rossum1677e5b1997-05-26 00:07:18 +000098 return string.join(list, '')
99
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000100def emphasize(line):
Guido van Rossum80e57fb1997-12-21 07:05:32 +0000101 return re.sub(r'\*([a-zA-Z]+)\*', r'<I>\1</I>', line)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000102
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000103revparse_prog = None
104
105def revparse(rev):
106 global revparse_prog
107 if not revparse_prog:
Guido van Rossum55b40b01998-05-22 19:43:21 +0000108 revparse_prog = re.compile(r'^(\d{1,3})\.(\d{1,4})$')
Guido van Rossum80e57fb1997-12-21 07:05:32 +0000109 m = revparse_prog.match(rev)
110 if not m:
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000111 return None
Guido van Rossum80e57fb1997-12-21 07:05:32 +0000112 [major, minor] = map(string.atoi, m.group(1, 2))
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000113 return major, minor
114
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000115def load_cookies():
116 if not os.environ.has_key('HTTP_COOKIE'):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000117 return {}
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000118 raw = os.environ['HTTP_COOKIE']
119 words = map(string.strip, string.split(raw, ';'))
120 cookies = {}
121 for word in words:
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000122 i = string.find(word, '=')
123 if i >= 0:
124 key, value = word[:i], word[i+1:]
125 cookies[key] = value
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000126 return cookies
127
128def load_my_cookie():
129 cookies = load_cookies()
130 try:
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000131 value = cookies[COOKIE_NAME]
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000132 except KeyError:
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000133 return {}
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000134 import urllib
135 value = urllib.unquote(value)
136 words = string.split(value, '/')
137 while len(words) < 3:
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000138 words.append('')
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000139 author = string.join(words[:-2], '/')
140 email = words[-2]
141 password = words[-1]
142 return {'author': author,
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000143 'email': email,
144 'password': password}
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000145
Guido van Rossumea31ea21997-05-26 05:43:29 +0000146def send_my_cookie(ui):
147 name = COOKIE_NAME
148 value = "%s/%s/%s" % (ui.author, ui.email, ui.password)
149 import urllib
150 value = urllib.quote(value)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000151 then = now + COOKIE_LIFETIME
152 gmt = time.gmtime(then)
Guido van Rossum2053aa61998-09-04 21:19:55 +0000153 path = os.environ.get('SCRIPT_NAME', '/cgi-bin/')
154 print "Set-Cookie: %s=%s; path=%s;" % (name, value, path),
Guido van Rossum48b1cde1998-02-02 03:19:06 +0000155 print time.strftime("expires=%a, %d-%b-%y %X GMT", gmt)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000156
Guido van Rossumea31ea21997-05-26 05:43:29 +0000157class MagicDict:
158
159 def __init__(self, d, quote):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000160 self.__d = d
161 self.__quote = quote
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000162
163 def __getitem__(self, key):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000164 for d in self.__d:
165 try:
166 value = d[key]
167 if value:
168 value = str(value)
169 if self.__quote:
170 value = escapeq(value)
171 return value
172 except KeyError:
173 pass
174 return ''
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000175
176class UserInput:
177
178 def __init__(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000179 self.__form = cgi.FieldStorage()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000180
181 def __getattr__(self, name):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000182 if name[0] == '_':
183 raise AttributeError
184 try:
185 value = self.__form[name].value
186 except (TypeError, KeyError):
187 value = ''
188 else:
189 value = string.strip(value)
190 setattr(self, name, value)
191 return value
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000192
193 def __getitem__(self, key):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000194 return getattr(self, key)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000195
Guido van Rossumea31ea21997-05-26 05:43:29 +0000196class FaqEntry:
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000197
Guido van Rossumea31ea21997-05-26 05:43:29 +0000198 def __init__(self, fp, file, sec_num):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000199 self.file = file
200 self.sec, self.num = sec_num
201 if fp:
202 import rfc822
203 self.__headers = rfc822.Message(fp)
204 self.body = string.strip(fp.read())
205 else:
206 self.__headers = {'title': "%d.%d. " % sec_num}
207 self.body = ''
Guido van Rossumea31ea21997-05-26 05:43:29 +0000208
209 def __getattr__(self, name):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000210 if name[0] == '_':
211 raise AttributeError
212 key = string.join(string.split(name, '_'), '-')
213 try:
214 value = self.__headers[key]
215 except KeyError:
216 value = ''
217 setattr(self, name, value)
218 return value
Guido van Rossumea31ea21997-05-26 05:43:29 +0000219
220 def __getitem__(self, key):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000221 return getattr(self, key)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000222
223 def load_version(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000224 command = interpolate(SH_RLOG_H, self)
225 p = os.popen(command)
226 version = ''
227 while 1:
228 line = p.readline()
229 if not line:
230 break
231 if line[:5] == 'head:':
232 version = string.strip(line[5:])
233 p.close()
234 self.version = version
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000235
Guido van Rossum7a241071997-05-26 19:46:56 +0000236 def getmtime(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000237 if not self.last_changed_date:
238 return 0
239 try:
240 return os.stat(self.file)[stat.ST_MTIME]
241 except os.error:
242 return 0
Guido van Rossum7a241071997-05-26 19:46:56 +0000243
244 def emit_marks(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000245 mtime = self.getmtime()
246 if mtime >= now - DT_VERY_RECENT:
247 emit(MARK_VERY_RECENT, self)
248 elif mtime >= now - DT_RECENT:
249 emit(MARK_RECENT, self)
Guido van Rossum7a241071997-05-26 19:46:56 +0000250
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000251 def show(self, edit=1):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000252 emit(ENTRY_HEADER1, self)
253 self.emit_marks()
254 emit(ENTRY_HEADER2, self)
255 pre = 0
256 raw = 0
257 for line in string.split(self.body, '\n'):
258 # Allow the user to insert raw html into a FAQ answer
259 # (Skip Montanaro, with changes by Guido)
260 tag = string.lower(string.rstrip(line))
261 if tag == '<html>':
262 raw = 1
263 continue
264 if tag == '</html>':
265 raw = 0
266 continue
267 if raw:
268 print line
269 continue
270 if not string.strip(line):
271 if pre:
272 print '</PRE>'
273 pre = 0
274 else:
275 print '<P>'
276 else:
277 if line[0] not in string.whitespace:
278 if pre:
279 print '</PRE>'
280 pre = 0
281 else:
282 if not pre:
283 print '<PRE>'
284 pre = 1
285 if '/' in line or '@' in line:
286 line = translate(line, pre)
287 elif '<' in line or '&' in line:
288 line = escape(line)
289 if not pre and '*' in line:
290 line = emphasize(line)
291 print line
292 if pre:
293 print '</PRE>'
294 pre = 0
295 if edit:
296 print '<P>'
297 emit(ENTRY_FOOTER, self)
298 if self.last_changed_date:
299 emit(ENTRY_LOGINFO, self)
300 print '<P>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000301
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000302class FaqDir:
303
304 entryclass = FaqEntry
305
Guido van Rossum80e57fb1997-12-21 07:05:32 +0000306 __okprog = re.compile(OKFILENAME)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000307
308 def __init__(self, dir=os.curdir):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000309 self.__dir = dir
310 self.__files = None
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000311
312 def __fill(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000313 if self.__files is not None:
314 return
315 self.__files = files = []
316 okprog = self.__okprog
317 for file in os.listdir(self.__dir):
318 if self.__okprog.match(file):
319 files.append(file)
320 files.sort()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000321
322 def good(self, file):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000323 return self.__okprog.match(file)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000324
325 def parse(self, file):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000326 m = self.good(file)
327 if not m:
328 return None
329 sec, num = m.group(1, 2)
330 return string.atoi(sec), string.atoi(num)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000331
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000332 def list(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000333 # XXX Caller shouldn't modify result
334 self.__fill()
335 return self.__files
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000336
337 def open(self, file):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000338 sec_num = self.parse(file)
339 if not sec_num:
340 raise InvalidFile(file)
341 try:
342 fp = open(file)
343 except IOError, msg:
344 raise NoSuchFile(file, msg)
345 try:
346 return self.entryclass(fp, file, sec_num)
347 finally:
348 fp.close()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000349
350 def show(self, file, edit=1):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000351 self.open(file).show(edit=edit)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000352
Guido van Rossumea31ea21997-05-26 05:43:29 +0000353 def new(self, section):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000354 if not SECTION_TITLES.has_key(section):
355 raise NoSuchSection(section)
356 maxnum = 0
357 for file in self.list():
358 sec, num = self.parse(file)
359 if sec == section:
360 maxnum = max(maxnum, num)
361 sec_num = (section, maxnum+1)
362 file = NEWFILENAME % sec_num
363 return self.entryclass(None, file, sec_num)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000364
365class FaqWizard:
366
367 def __init__(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000368 self.ui = UserInput()
369 self.dir = FaqDir()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000370
371 def go(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000372 print 'Content-type: text/html'
373 req = self.ui.req or 'home'
374 mname = 'do_%s' % req
375 try:
376 meth = getattr(self, mname)
377 except AttributeError:
378 self.error("Bad request type %s." % `req`)
379 else:
380 try:
381 meth()
382 except InvalidFile, exc:
383 self.error("Invalid entry file name %s" % exc.file)
384 except NoSuchFile, exc:
385 self.error("No entry with file name %s" % exc.file)
386 except NoSuchSection, exc:
387 self.error("No section number %s" % exc.section)
388 self.epilogue()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000389
390 def error(self, message, **kw):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000391 self.prologue(T_ERROR)
392 emit(message, kw)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000393
394 def prologue(self, title, entry=None, **kw):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000395 emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000396
397 def epilogue(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000398 emit(EPILOGUE)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000399
400 def do_home(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000401 self.prologue(T_HOME)
402 emit(HOME)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000403
404 def do_debug(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000405 self.prologue("FAQ Wizard Debugging")
406 form = cgi.FieldStorage()
407 cgi.print_form(form)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000408 cgi.print_environ(os.environ)
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000409 cgi.print_directory()
410 cgi.print_arguments()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000411
412 def do_search(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000413 query = self.ui.query
414 if not query:
415 self.error("Empty query string!")
416 return
417 if self.ui.querytype == 'simple':
418 query = re.escape(query)
419 queries = [query]
420 elif self.ui.querytype in ('anykeywords', 'allkeywords'):
421 words = filter(None, re.split('\W+', query))
422 if not words:
423 self.error("No keywords specified!")
424 return
425 words = map(lambda w: r'\b%s\b' % w, words)
426 if self.ui.querytype[:3] == 'any':
427 queries = [string.join(words, '|')]
428 else:
429 # Each of the individual queries must match
430 queries = words
431 else:
432 # Default to regular expression
433 queries = [query]
434 self.prologue(T_SEARCH)
435 progs = []
436 for query in queries:
437 if self.ui.casefold == 'no':
438 p = re.compile(query)
439 else:
440 p = re.compile(query, re.IGNORECASE)
441 progs.append(p)
442 hits = []
443 for file in self.dir.list():
444 try:
445 entry = self.dir.open(file)
446 except FileError:
447 constants
448 for p in progs:
449 if not p.search(entry.title) and not p.search(entry.body):
450 break
451 else:
452 hits.append(file)
453 if not hits:
454 emit(NO_HITS, self.ui, count=0)
455 elif len(hits) <= MAXHITS:
456 if len(hits) == 1:
457 emit(ONE_HIT, count=1)
458 else:
459 emit(FEW_HITS, count=len(hits))
460 self.format_all(hits, headers=0)
461 else:
462 emit(MANY_HITS, count=len(hits))
463 self.format_index(hits)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000464
465 def do_all(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000466 self.prologue(T_ALL)
467 files = self.dir.list()
468 self.last_changed(files)
469 self.format_index(files, localrefs=1)
470 self.format_all(files)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000471
472 def do_compat(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000473 files = self.dir.list()
474 emit(COMPAT)
475 self.last_changed(files)
476 self.format_index(files, localrefs=1)
477 self.format_all(files, edit=0)
478 sys.exit(0) # XXX Hack to suppress epilogue
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000479
480 def last_changed(self, files):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000481 latest = 0
482 for file in files:
483 entry = self.dir.open(file)
484 if entry:
485 mtime = mtime = entry.getmtime()
486 if mtime > latest:
487 latest = mtime
488 print time.strftime(LAST_CHANGED, time.localtime(latest))
489 emit(EXPLAIN_MARKS)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000490
Guido van Rossum21c4b5f1997-05-26 06:28:40 +0000491 def format_all(self, files, edit=1, headers=1):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000492 sec = 0
493 for file in files:
494 try:
495 entry = self.dir.open(file)
496 except NoSuchFile:
497 continue
498 if headers and entry.sec != sec:
499 sec = entry.sec
500 try:
501 title = SECTION_TITLES[sec]
502 except KeyError:
503 title = "Untitled"
504 emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n",
505 sec=sec, title=title)
506 entry.show(edit=edit)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000507
508 def do_index(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000509 self.prologue(T_INDEX)
510 files = self.dir.list()
511 self.last_changed(files)
512 self.format_index(files, add=1)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000513
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000514 def format_index(self, files, add=0, localrefs=0):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000515 sec = 0
516 for file in files:
517 try:
518 entry = self.dir.open(file)
519 except NoSuchFile:
520 continue
521 if entry.sec != sec:
522 if sec:
523 if add:
524 emit(INDEX_ADDSECTION, sec=sec)
525 emit(INDEX_ENDSECTION, sec=sec)
526 sec = entry.sec
527 try:
528 title = SECTION_TITLES[sec]
529 except KeyError:
530 title = "Untitled"
531 emit(INDEX_SECTION, sec=sec, title=title)
532 if localrefs:
533 emit(LOCAL_ENTRY, entry)
534 else:
535 emit(INDEX_ENTRY, entry)
536 entry.emit_marks()
537 if sec:
538 if add:
539 emit(INDEX_ADDSECTION, sec=sec)
540 emit(INDEX_ENDSECTION, sec=sec)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000541
542 def do_recent(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000543 if not self.ui.days:
544 days = 1
545 else:
546 days = string.atof(self.ui.days)
547 try:
548 cutoff = now - days * 24 * 3600
549 except OverflowError:
550 cutoff = 0
551 list = []
552 for file in self.dir.list():
553 entry = self.dir.open(file)
554 if not entry:
555 continue
556 mtime = entry.getmtime()
557 if mtime >= cutoff:
558 list.append((mtime, file))
559 list.sort()
560 list.reverse()
561 self.prologue(T_RECENT)
562 if days <= 1:
563 period = "%.2g hours" % (days*24)
564 else:
565 period = "%.6g days" % days
566 if not list:
567 emit(NO_RECENT, period=period)
568 elif len(list) == 1:
569 emit(ONE_RECENT, period=period)
570 else:
571 emit(SOME_RECENT, period=period, count=len(list))
572 self.format_all(map(lambda (mtime, file): file, list), headers=0)
573 emit(TAIL_RECENT)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000574
575 def do_roulette(self):
Guido van Rossum6c3a2cb1998-05-20 17:13:01 +0000576 import random
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000577 files = self.dir.list()
578 if not files:
579 self.error("No entries.")
580 return
Guido van Rossum6c3a2cb1998-05-20 17:13:01 +0000581 file = random.choice(files)
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000582 self.prologue(T_ROULETTE)
583 emit(ROULETTE)
584 self.dir.show(file)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000585
586 def do_help(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000587 self.prologue(T_HELP)
588 emit(HELP)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000589
590 def do_show(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000591 entry = self.dir.open(self.ui.file)
592 self.prologue(T_SHOW)
593 entry.show()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000594
595 def do_add(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000596 self.prologue(T_ADD)
597 emit(ADD_HEAD)
598 sections = SECTION_TITLES.items()
599 sections.sort()
600 for section, title in sections:
601 emit(ADD_SECTION, section=section, title=title)
602 emit(ADD_TAIL)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000603
604 def do_delete(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000605 self.prologue(T_DELETE)
606 emit(DELETE)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000607
608 def do_log(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000609 entry = self.dir.open(self.ui.file)
610 self.prologue(T_LOG, entry)
611 emit(LOG, entry)
612 self.rlog(interpolate(SH_RLOG, entry), entry)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000613
614 def rlog(self, command, entry=None):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000615 output = os.popen(command).read()
616 sys.stdout.write('<PRE>')
617 athead = 0
618 lines = string.split(output, '\n')
619 while lines and not lines[-1]:
620 del lines[-1]
621 if lines:
622 line = lines[-1]
623 if line[:1] == '=' and len(line) >= 40 and \
624 line == line[0]*len(line):
625 del lines[-1]
626 headrev = None
627 for line in lines:
628 if entry and athead and line[:9] == 'revision ':
629 rev = string.strip(line[9:])
630 mami = revparse(rev)
631 if not mami:
632 print line
633 else:
634 emit(REVISIONLINK, entry, rev=rev, line=line)
635 if mami[1] > 1:
636 prev = "%d.%d" % (mami[0], mami[1]-1)
637 emit(DIFFLINK, entry, prev=prev, rev=rev)
638 if headrev:
639 emit(DIFFLINK, entry, prev=rev, rev=headrev)
640 else:
641 headrev = rev
642 print
643 athead = 0
644 else:
645 athead = 0
646 if line[:1] == '-' and len(line) >= 20 and \
647 line == len(line) * line[0]:
648 athead = 1
649 sys.stdout.write('<HR>')
650 else:
651 print line
652 print '</PRE>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000653
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000654 def do_revision(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000655 entry = self.dir.open(self.ui.file)
656 rev = self.ui.rev
657 mami = revparse(rev)
658 if not mami:
659 self.error("Invalid revision number: %s." % `rev`)
660 self.prologue(T_REVISION, entry)
661 self.shell(interpolate(SH_REVISION, entry, rev=rev))
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000662
663 def do_diff(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000664 entry = self.dir.open(self.ui.file)
665 prev = self.ui.prev
666 rev = self.ui.rev
667 mami = revparse(rev)
668 if not mami:
669 self.error("Invalid revision number: %s." % `rev`)
670 if prev:
671 if not revparse(prev):
672 self.error("Invalid previous revision number: %s." % `prev`)
673 else:
674 prev = '%d.%d' % (mami[0], mami[1])
675 self.prologue(T_DIFF, entry)
676 self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000677
678 def shell(self, command):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000679 output = os.popen(command).read()
680 sys.stdout.write('<PRE>')
681 print escape(output)
682 print '</PRE>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000683
684 def do_new(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000685 entry = self.dir.new(section=string.atoi(self.ui.section))
686 entry.version = '*new*'
687 self.prologue(T_EDIT)
688 emit(EDITHEAD)
689 emit(EDITFORM1, entry, editversion=entry.version)
690 emit(EDITFORM2, entry, load_my_cookie())
691 emit(EDITFORM3)
692 entry.show(edit=0)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000693
694 def do_edit(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000695 entry = self.dir.open(self.ui.file)
696 entry.load_version()
697 self.prologue(T_EDIT)
698 emit(EDITHEAD)
699 emit(EDITFORM1, entry, editversion=entry.version)
700 emit(EDITFORM2, entry, load_my_cookie())
701 emit(EDITFORM3)
702 entry.show(edit=0)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000703
704 def do_review(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000705 send_my_cookie(self.ui)
706 if self.ui.editversion == '*new*':
707 sec, num = self.dir.parse(self.ui.file)
708 entry = self.dir.new(section=sec)
709 entry.version = "*new*"
710 if entry.file != self.ui.file:
711 self.error("Commit version conflict!")
712 emit(NEWCONFLICT, self.ui, sec=sec, num=num)
713 return
714 else:
715 entry = self.dir.open(self.ui.file)
716 entry.load_version()
717 # Check that the FAQ entry number didn't change
718 if string.split(self.ui.title)[:1] != string.split(entry.title)[:1]:
719 self.error("Don't change the entry number please!")
720 return
721 # Check that the edited version is the current version
722 if entry.version != self.ui.editversion:
723 self.error("Commit version conflict!")
724 emit(VERSIONCONFLICT, entry, self.ui)
725 return
726 commit_ok = ((not PASSWORD
727 or self.ui.password == PASSWORD)
728 and self.ui.author
729 and '@' in self.ui.email
730 and self.ui.log)
731 if self.ui.commit:
732 if not commit_ok:
733 self.cantcommit()
734 else:
735 self.commit(entry)
736 return
737 self.prologue(T_REVIEW)
738 emit(REVIEWHEAD)
739 entry.body = self.ui.body
740 entry.title = self.ui.title
741 entry.show(edit=0)
742 emit(EDITFORM1, self.ui, entry)
743 if commit_ok:
744 emit(COMMIT)
745 else:
Guido van Rossum2d3b0d71998-12-23 21:33:09 +0000746 emit(NOCOMMIT_HEAD)
747 self.errordetail()
748 emit(NOCOMMIT_TAIL)
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000749 emit(EDITFORM2, self.ui, entry, load_my_cookie())
750 emit(EDITFORM3)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000751
752 def cantcommit(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000753 self.prologue(T_CANTCOMMIT)
754 print CANTCOMMIT_HEAD
Guido van Rossum2d3b0d71998-12-23 21:33:09 +0000755 self.errordetail()
756 print CANTCOMMIT_TAIL
757
758 def errordetail(self):
759 if PASSWORD and self.ui.password != PASSWORD:
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000760 emit(NEED_PASSWD)
761 if not self.ui.log:
762 emit(NEED_LOG)
763 if not self.ui.author:
764 emit(NEED_AUTHOR)
765 if not self.ui.email:
766 emit(NEED_EMAIL)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000767
Guido van Rossumea31ea21997-05-26 05:43:29 +0000768 def commit(self, entry):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000769 file = entry.file
770 # Normalize line endings in body
771 if '\r' in self.ui.body:
772 self.ui.body = re.sub('\r\n?', '\n', self.ui.body)
773 # Normalize whitespace in title
774 self.ui.title = string.join(string.split(self.ui.title))
775 # Check that there were any changes
776 if self.ui.body == entry.body and self.ui.title == entry.title:
777 self.error("You didn't make any changes!")
778 return
779 # XXX Should lock here
780 try:
781 os.unlink(file)
782 except os.error:
783 pass
784 try:
785 f = open(file, 'w')
786 except IOError, why:
787 self.error(CANTWRITE, file=file, why=why)
788 return
789 date = time.ctime(now)
790 emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0)
791 f.write('\n')
792 f.write(self.ui.body)
793 f.write('\n')
794 f.close()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000795
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000796 import tempfile
797 tfn = tempfile.mktemp()
798 f = open(tfn, 'w')
799 emit(LOGHEADER, self.ui, os.environ, date=date, _file=f)
800 f.close()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000801
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000802 command = interpolate(
803 SH_LOCK + '\n' + SH_CHECKIN,
804 file=file, tfn=tfn)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000805
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000806 p = os.popen(command)
807 output = p.read()
808 sts = p.close()
809 # XXX Should unlock here
810 if not sts:
811 self.prologue(T_COMMITTED)
812 emit(COMMITTED)
813 else:
814 self.error(T_COMMITFAILED)
815 emit(COMMITFAILED, sts=sts)
816 print '<PRE>%s</PRE>' % escape(output)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000817
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000818 try:
819 os.unlink(tfn)
820 except os.error:
821 pass
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000822
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000823 entry = self.dir.open(file)
824 entry.show()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000825
826wiz = FaqWizard()
827wiz.go()