blob: f604fd2235fd36cba70b517fcaa126ce3bc160f5 [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:
92 repl = '<A HREF="mailto:%s">&lt;%s&gt;</A>' % (url, url)
93 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 Rossum72dc60c1998-04-06 14:24:36 +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)
153 print "Set-Cookie: %s=%s; path=/cgi-bin/;" % (name, value),
Guido van Rossum48b1cde1998-02-02 03:19:06 +0000154 print time.strftime("expires=%a, %d-%b-%y %X GMT", gmt)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000155
Guido van Rossumea31ea21997-05-26 05:43:29 +0000156class MagicDict:
157
158 def __init__(self, d, quote):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000159 self.__d = d
160 self.__quote = quote
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000161
162 def __getitem__(self, key):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000163 for d in self.__d:
164 try:
165 value = d[key]
166 if value:
167 value = str(value)
168 if self.__quote:
169 value = escapeq(value)
170 return value
171 except KeyError:
172 pass
173 return ''
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000174
175class UserInput:
176
177 def __init__(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000178 self.__form = cgi.FieldStorage()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000179
180 def __getattr__(self, name):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000181 if name[0] == '_':
182 raise AttributeError
183 try:
184 value = self.__form[name].value
185 except (TypeError, KeyError):
186 value = ''
187 else:
188 value = string.strip(value)
189 setattr(self, name, value)
190 return value
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000191
192 def __getitem__(self, key):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000193 return getattr(self, key)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000194
Guido van Rossumea31ea21997-05-26 05:43:29 +0000195class FaqEntry:
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000196
Guido van Rossumea31ea21997-05-26 05:43:29 +0000197 def __init__(self, fp, file, sec_num):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000198 self.file = file
199 self.sec, self.num = sec_num
200 if fp:
201 import rfc822
202 self.__headers = rfc822.Message(fp)
203 self.body = string.strip(fp.read())
204 else:
205 self.__headers = {'title': "%d.%d. " % sec_num}
206 self.body = ''
Guido van Rossumea31ea21997-05-26 05:43:29 +0000207
208 def __getattr__(self, name):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000209 if name[0] == '_':
210 raise AttributeError
211 key = string.join(string.split(name, '_'), '-')
212 try:
213 value = self.__headers[key]
214 except KeyError:
215 value = ''
216 setattr(self, name, value)
217 return value
Guido van Rossumea31ea21997-05-26 05:43:29 +0000218
219 def __getitem__(self, key):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000220 return getattr(self, key)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000221
222 def load_version(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000223 command = interpolate(SH_RLOG_H, self)
224 p = os.popen(command)
225 version = ''
226 while 1:
227 line = p.readline()
228 if not line:
229 break
230 if line[:5] == 'head:':
231 version = string.strip(line[5:])
232 p.close()
233 self.version = version
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000234
Guido van Rossum7a241071997-05-26 19:46:56 +0000235 def getmtime(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000236 if not self.last_changed_date:
237 return 0
238 try:
239 return os.stat(self.file)[stat.ST_MTIME]
240 except os.error:
241 return 0
Guido van Rossum7a241071997-05-26 19:46:56 +0000242
243 def emit_marks(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000244 mtime = self.getmtime()
245 if mtime >= now - DT_VERY_RECENT:
246 emit(MARK_VERY_RECENT, self)
247 elif mtime >= now - DT_RECENT:
248 emit(MARK_RECENT, self)
Guido van Rossum7a241071997-05-26 19:46:56 +0000249
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000250 def show(self, edit=1):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000251 emit(ENTRY_HEADER1, self)
252 self.emit_marks()
253 emit(ENTRY_HEADER2, self)
254 pre = 0
255 raw = 0
256 for line in string.split(self.body, '\n'):
257 # Allow the user to insert raw html into a FAQ answer
258 # (Skip Montanaro, with changes by Guido)
259 tag = string.lower(string.rstrip(line))
260 if tag == '<html>':
261 raw = 1
262 continue
263 if tag == '</html>':
264 raw = 0
265 continue
266 if raw:
267 print line
268 continue
269 if not string.strip(line):
270 if pre:
271 print '</PRE>'
272 pre = 0
273 else:
274 print '<P>'
275 else:
276 if line[0] not in string.whitespace:
277 if pre:
278 print '</PRE>'
279 pre = 0
280 else:
281 if not pre:
282 print '<PRE>'
283 pre = 1
284 if '/' in line or '@' in line:
285 line = translate(line, pre)
286 elif '<' in line or '&' in line:
287 line = escape(line)
288 if not pre and '*' in line:
289 line = emphasize(line)
290 print line
291 if pre:
292 print '</PRE>'
293 pre = 0
294 if edit:
295 print '<P>'
296 emit(ENTRY_FOOTER, self)
297 if self.last_changed_date:
298 emit(ENTRY_LOGINFO, self)
299 print '<P>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000300
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000301class FaqDir:
302
303 entryclass = FaqEntry
304
Guido van Rossum80e57fb1997-12-21 07:05:32 +0000305 __okprog = re.compile(OKFILENAME)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000306
307 def __init__(self, dir=os.curdir):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000308 self.__dir = dir
309 self.__files = None
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000310
311 def __fill(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000312 if self.__files is not None:
313 return
314 self.__files = files = []
315 okprog = self.__okprog
316 for file in os.listdir(self.__dir):
317 if self.__okprog.match(file):
318 files.append(file)
319 files.sort()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000320
321 def good(self, file):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000322 return self.__okprog.match(file)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000323
324 def parse(self, file):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000325 m = self.good(file)
326 if not m:
327 return None
328 sec, num = m.group(1, 2)
329 return string.atoi(sec), string.atoi(num)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000330
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000331 def list(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000332 # XXX Caller shouldn't modify result
333 self.__fill()
334 return self.__files
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000335
336 def open(self, file):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000337 sec_num = self.parse(file)
338 if not sec_num:
339 raise InvalidFile(file)
340 try:
341 fp = open(file)
342 except IOError, msg:
343 raise NoSuchFile(file, msg)
344 try:
345 return self.entryclass(fp, file, sec_num)
346 finally:
347 fp.close()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000348
349 def show(self, file, edit=1):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000350 self.open(file).show(edit=edit)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000351
Guido van Rossumea31ea21997-05-26 05:43:29 +0000352 def new(self, section):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000353 if not SECTION_TITLES.has_key(section):
354 raise NoSuchSection(section)
355 maxnum = 0
356 for file in self.list():
357 sec, num = self.parse(file)
358 if sec == section:
359 maxnum = max(maxnum, num)
360 sec_num = (section, maxnum+1)
361 file = NEWFILENAME % sec_num
362 return self.entryclass(None, file, sec_num)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000363
364class FaqWizard:
365
366 def __init__(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000367 self.ui = UserInput()
368 self.dir = FaqDir()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000369
370 def go(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000371 print 'Content-type: text/html'
372 req = self.ui.req or 'home'
373 mname = 'do_%s' % req
374 try:
375 meth = getattr(self, mname)
376 except AttributeError:
377 self.error("Bad request type %s." % `req`)
378 else:
379 try:
380 meth()
381 except InvalidFile, exc:
382 self.error("Invalid entry file name %s" % exc.file)
383 except NoSuchFile, exc:
384 self.error("No entry with file name %s" % exc.file)
385 except NoSuchSection, exc:
386 self.error("No section number %s" % exc.section)
387 self.epilogue()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000388
389 def error(self, message, **kw):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000390 self.prologue(T_ERROR)
391 emit(message, kw)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000392
393 def prologue(self, title, entry=None, **kw):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000394 emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000395
396 def epilogue(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000397 emit(EPILOGUE)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000398
399 def do_home(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000400 self.prologue(T_HOME)
401 emit(HOME)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000402
403 def do_debug(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000404 self.prologue("FAQ Wizard Debugging")
405 form = cgi.FieldStorage()
406 cgi.print_form(form)
Guido van Rossumea31ea21997-05-26 05:43:29 +0000407 cgi.print_environ(os.environ)
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000408 cgi.print_directory()
409 cgi.print_arguments()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000410
411 def do_search(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000412 query = self.ui.query
413 if not query:
414 self.error("Empty query string!")
415 return
416 if self.ui.querytype == 'simple':
417 query = re.escape(query)
418 queries = [query]
419 elif self.ui.querytype in ('anykeywords', 'allkeywords'):
420 words = filter(None, re.split('\W+', query))
421 if not words:
422 self.error("No keywords specified!")
423 return
424 words = map(lambda w: r'\b%s\b' % w, words)
425 if self.ui.querytype[:3] == 'any':
426 queries = [string.join(words, '|')]
427 else:
428 # Each of the individual queries must match
429 queries = words
430 else:
431 # Default to regular expression
432 queries = [query]
433 self.prologue(T_SEARCH)
434 progs = []
435 for query in queries:
436 if self.ui.casefold == 'no':
437 p = re.compile(query)
438 else:
439 p = re.compile(query, re.IGNORECASE)
440 progs.append(p)
441 hits = []
442 for file in self.dir.list():
443 try:
444 entry = self.dir.open(file)
445 except FileError:
446 constants
447 for p in progs:
448 if not p.search(entry.title) and not p.search(entry.body):
449 break
450 else:
451 hits.append(file)
452 if not hits:
453 emit(NO_HITS, self.ui, count=0)
454 elif len(hits) <= MAXHITS:
455 if len(hits) == 1:
456 emit(ONE_HIT, count=1)
457 else:
458 emit(FEW_HITS, count=len(hits))
459 self.format_all(hits, headers=0)
460 else:
461 emit(MANY_HITS, count=len(hits))
462 self.format_index(hits)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000463
464 def do_all(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000465 self.prologue(T_ALL)
466 files = self.dir.list()
467 self.last_changed(files)
468 self.format_index(files, localrefs=1)
469 self.format_all(files)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000470
471 def do_compat(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000472 files = self.dir.list()
473 emit(COMPAT)
474 self.last_changed(files)
475 self.format_index(files, localrefs=1)
476 self.format_all(files, edit=0)
477 sys.exit(0) # XXX Hack to suppress epilogue
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000478
479 def last_changed(self, files):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000480 latest = 0
481 for file in files:
482 entry = self.dir.open(file)
483 if entry:
484 mtime = mtime = entry.getmtime()
485 if mtime > latest:
486 latest = mtime
487 print time.strftime(LAST_CHANGED, time.localtime(latest))
488 emit(EXPLAIN_MARKS)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000489
Guido van Rossum21c4b5f1997-05-26 06:28:40 +0000490 def format_all(self, files, edit=1, headers=1):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000491 sec = 0
492 for file in files:
493 try:
494 entry = self.dir.open(file)
495 except NoSuchFile:
496 continue
497 if headers and entry.sec != sec:
498 sec = entry.sec
499 try:
500 title = SECTION_TITLES[sec]
501 except KeyError:
502 title = "Untitled"
503 emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n",
504 sec=sec, title=title)
505 entry.show(edit=edit)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000506
507 def do_index(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000508 self.prologue(T_INDEX)
509 files = self.dir.list()
510 self.last_changed(files)
511 self.format_index(files, add=1)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000512
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000513 def format_index(self, files, add=0, localrefs=0):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000514 sec = 0
515 for file in files:
516 try:
517 entry = self.dir.open(file)
518 except NoSuchFile:
519 continue
520 if entry.sec != sec:
521 if sec:
522 if add:
523 emit(INDEX_ADDSECTION, sec=sec)
524 emit(INDEX_ENDSECTION, sec=sec)
525 sec = entry.sec
526 try:
527 title = SECTION_TITLES[sec]
528 except KeyError:
529 title = "Untitled"
530 emit(INDEX_SECTION, sec=sec, title=title)
531 if localrefs:
532 emit(LOCAL_ENTRY, entry)
533 else:
534 emit(INDEX_ENTRY, entry)
535 entry.emit_marks()
536 if sec:
537 if add:
538 emit(INDEX_ADDSECTION, sec=sec)
539 emit(INDEX_ENDSECTION, sec=sec)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000540
541 def do_recent(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000542 if not self.ui.days:
543 days = 1
544 else:
545 days = string.atof(self.ui.days)
546 try:
547 cutoff = now - days * 24 * 3600
548 except OverflowError:
549 cutoff = 0
550 list = []
551 for file in self.dir.list():
552 entry = self.dir.open(file)
553 if not entry:
554 continue
555 mtime = entry.getmtime()
556 if mtime >= cutoff:
557 list.append((mtime, file))
558 list.sort()
559 list.reverse()
560 self.prologue(T_RECENT)
561 if days <= 1:
562 period = "%.2g hours" % (days*24)
563 else:
564 period = "%.6g days" % days
565 if not list:
566 emit(NO_RECENT, period=period)
567 elif len(list) == 1:
568 emit(ONE_RECENT, period=period)
569 else:
570 emit(SOME_RECENT, period=period, count=len(list))
571 self.format_all(map(lambda (mtime, file): file, list), headers=0)
572 emit(TAIL_RECENT)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000573
574 def do_roulette(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000575 import whrandom
576 files = self.dir.list()
577 if not files:
578 self.error("No entries.")
579 return
580 file = whrandom.choice(files)
581 self.prologue(T_ROULETTE)
582 emit(ROULETTE)
583 self.dir.show(file)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000584
585 def do_help(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000586 self.prologue(T_HELP)
587 emit(HELP)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000588
589 def do_show(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000590 entry = self.dir.open(self.ui.file)
591 self.prologue(T_SHOW)
592 entry.show()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000593
594 def do_add(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000595 self.prologue(T_ADD)
596 emit(ADD_HEAD)
597 sections = SECTION_TITLES.items()
598 sections.sort()
599 for section, title in sections:
600 emit(ADD_SECTION, section=section, title=title)
601 emit(ADD_TAIL)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000602
603 def do_delete(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000604 self.prologue(T_DELETE)
605 emit(DELETE)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000606
607 def do_log(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000608 entry = self.dir.open(self.ui.file)
609 self.prologue(T_LOG, entry)
610 emit(LOG, entry)
611 self.rlog(interpolate(SH_RLOG, entry), entry)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000612
613 def rlog(self, command, entry=None):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000614 output = os.popen(command).read()
615 sys.stdout.write('<PRE>')
616 athead = 0
617 lines = string.split(output, '\n')
618 while lines and not lines[-1]:
619 del lines[-1]
620 if lines:
621 line = lines[-1]
622 if line[:1] == '=' and len(line) >= 40 and \
623 line == line[0]*len(line):
624 del lines[-1]
625 headrev = None
626 for line in lines:
627 if entry and athead and line[:9] == 'revision ':
628 rev = string.strip(line[9:])
629 mami = revparse(rev)
630 if not mami:
631 print line
632 else:
633 emit(REVISIONLINK, entry, rev=rev, line=line)
634 if mami[1] > 1:
635 prev = "%d.%d" % (mami[0], mami[1]-1)
636 emit(DIFFLINK, entry, prev=prev, rev=rev)
637 if headrev:
638 emit(DIFFLINK, entry, prev=rev, rev=headrev)
639 else:
640 headrev = rev
641 print
642 athead = 0
643 else:
644 athead = 0
645 if line[:1] == '-' and len(line) >= 20 and \
646 line == len(line) * line[0]:
647 athead = 1
648 sys.stdout.write('<HR>')
649 else:
650 print line
651 print '</PRE>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000652
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000653 def do_revision(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000654 entry = self.dir.open(self.ui.file)
655 rev = self.ui.rev
656 mami = revparse(rev)
657 if not mami:
658 self.error("Invalid revision number: %s." % `rev`)
659 self.prologue(T_REVISION, entry)
660 self.shell(interpolate(SH_REVISION, entry, rev=rev))
Guido van Rossum8bc49c81997-05-26 19:10:37 +0000661
662 def do_diff(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000663 entry = self.dir.open(self.ui.file)
664 prev = self.ui.prev
665 rev = self.ui.rev
666 mami = revparse(rev)
667 if not mami:
668 self.error("Invalid revision number: %s." % `rev`)
669 if prev:
670 if not revparse(prev):
671 self.error("Invalid previous revision number: %s." % `prev`)
672 else:
673 prev = '%d.%d' % (mami[0], mami[1])
674 self.prologue(T_DIFF, entry)
675 self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000676
677 def shell(self, command):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000678 output = os.popen(command).read()
679 sys.stdout.write('<PRE>')
680 print escape(output)
681 print '</PRE>'
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000682
683 def do_new(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000684 entry = self.dir.new(section=string.atoi(self.ui.section))
685 entry.version = '*new*'
686 self.prologue(T_EDIT)
687 emit(EDITHEAD)
688 emit(EDITFORM1, entry, editversion=entry.version)
689 emit(EDITFORM2, entry, load_my_cookie())
690 emit(EDITFORM3)
691 entry.show(edit=0)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000692
693 def do_edit(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000694 entry = self.dir.open(self.ui.file)
695 entry.load_version()
696 self.prologue(T_EDIT)
697 emit(EDITHEAD)
698 emit(EDITFORM1, entry, editversion=entry.version)
699 emit(EDITFORM2, entry, load_my_cookie())
700 emit(EDITFORM3)
701 entry.show(edit=0)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000702
703 def do_review(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000704 send_my_cookie(self.ui)
705 if self.ui.editversion == '*new*':
706 sec, num = self.dir.parse(self.ui.file)
707 entry = self.dir.new(section=sec)
708 entry.version = "*new*"
709 if entry.file != self.ui.file:
710 self.error("Commit version conflict!")
711 emit(NEWCONFLICT, self.ui, sec=sec, num=num)
712 return
713 else:
714 entry = self.dir.open(self.ui.file)
715 entry.load_version()
716 # Check that the FAQ entry number didn't change
717 if string.split(self.ui.title)[:1] != string.split(entry.title)[:1]:
718 self.error("Don't change the entry number please!")
719 return
720 # Check that the edited version is the current version
721 if entry.version != self.ui.editversion:
722 self.error("Commit version conflict!")
723 emit(VERSIONCONFLICT, entry, self.ui)
724 return
725 commit_ok = ((not PASSWORD
726 or self.ui.password == PASSWORD)
727 and self.ui.author
728 and '@' in self.ui.email
729 and self.ui.log)
730 if self.ui.commit:
731 if not commit_ok:
732 self.cantcommit()
733 else:
734 self.commit(entry)
735 return
736 self.prologue(T_REVIEW)
737 emit(REVIEWHEAD)
738 entry.body = self.ui.body
739 entry.title = self.ui.title
740 entry.show(edit=0)
741 emit(EDITFORM1, self.ui, entry)
742 if commit_ok:
743 emit(COMMIT)
744 else:
745 emit(NOCOMMIT)
746 emit(EDITFORM2, self.ui, entry, load_my_cookie())
747 emit(EDITFORM3)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000748
749 def cantcommit(self):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000750 self.prologue(T_CANTCOMMIT)
751 print CANTCOMMIT_HEAD
752 if not self.ui.passwd:
753 emit(NEED_PASSWD)
754 if not self.ui.log:
755 emit(NEED_LOG)
756 if not self.ui.author:
757 emit(NEED_AUTHOR)
758 if not self.ui.email:
759 emit(NEED_EMAIL)
760 print CANTCOMMIT_TAIL
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000761
Guido van Rossumea31ea21997-05-26 05:43:29 +0000762 def commit(self, entry):
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000763 file = entry.file
764 # Normalize line endings in body
765 if '\r' in self.ui.body:
766 self.ui.body = re.sub('\r\n?', '\n', self.ui.body)
767 # Normalize whitespace in title
768 self.ui.title = string.join(string.split(self.ui.title))
769 # Check that there were any changes
770 if self.ui.body == entry.body and self.ui.title == entry.title:
771 self.error("You didn't make any changes!")
772 return
773 # XXX Should lock here
774 try:
775 os.unlink(file)
776 except os.error:
777 pass
778 try:
779 f = open(file, 'w')
780 except IOError, why:
781 self.error(CANTWRITE, file=file, why=why)
782 return
783 date = time.ctime(now)
784 emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0)
785 f.write('\n')
786 f.write(self.ui.body)
787 f.write('\n')
788 f.close()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000789
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000790 import tempfile
791 tfn = tempfile.mktemp()
792 f = open(tfn, 'w')
793 emit(LOGHEADER, self.ui, os.environ, date=date, _file=f)
794 f.close()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000795
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000796 command = interpolate(
797 SH_LOCK + '\n' + SH_CHECKIN,
798 file=file, tfn=tfn)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000799
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000800 p = os.popen(command)
801 output = p.read()
802 sts = p.close()
803 # XXX Should unlock here
804 if not sts:
805 self.prologue(T_COMMITTED)
806 emit(COMMITTED)
807 else:
808 self.error(T_COMMITFAILED)
809 emit(COMMITFAILED, sts=sts)
810 print '<PRE>%s</PRE>' % escape(output)
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000811
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000812 try:
813 os.unlink(tfn)
814 except os.error:
815 pass
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000816
Guido van Rossum72dc60c1998-04-06 14:24:36 +0000817 entry = self.dir.open(file)
818 entry.show()
Guido van Rossum1677e5b1997-05-26 00:07:18 +0000819
820wiz = FaqWizard()
821wiz.go()