|
1 """Generic FAQ Wizard. |
|
2 |
|
3 This is a CGI program that maintains a user-editable FAQ. It uses RCS |
|
4 to keep track of changes to individual FAQ entries. It is fully |
|
5 configurable; everything you might want to change when using this |
|
6 program to maintain some other FAQ than the Python FAQ is contained in |
|
7 the configuration module, faqconf.py. |
|
8 |
|
9 Note that this is not an executable script; it's an importable module. |
|
10 The actual script to place in cgi-bin is faqw.py. |
|
11 |
|
12 """ |
|
13 |
|
14 import sys, time, os, stat, re, cgi, faqconf |
|
15 from faqconf import * # This imports all uppercase names |
|
16 now = time.time() |
|
17 |
|
18 class FileError: |
|
19 def __init__(self, file): |
|
20 self.file = file |
|
21 |
|
22 class InvalidFile(FileError): |
|
23 pass |
|
24 |
|
25 class NoSuchSection(FileError): |
|
26 def __init__(self, section): |
|
27 FileError.__init__(self, NEWFILENAME %(section, 1)) |
|
28 self.section = section |
|
29 |
|
30 class NoSuchFile(FileError): |
|
31 def __init__(self, file, why=None): |
|
32 FileError.__init__(self, file) |
|
33 self.why = why |
|
34 |
|
35 def escape(s): |
|
36 s = s.replace('&', '&') |
|
37 s = s.replace('<', '<') |
|
38 s = s.replace('>', '>') |
|
39 return s |
|
40 |
|
41 def escapeq(s): |
|
42 s = escape(s) |
|
43 s = s.replace('"', '"') |
|
44 return s |
|
45 |
|
46 def _interpolate(format, args, kw): |
|
47 try: |
|
48 quote = kw['_quote'] |
|
49 except KeyError: |
|
50 quote = 1 |
|
51 d = (kw,) + args + (faqconf.__dict__,) |
|
52 m = MagicDict(d, quote) |
|
53 return format % m |
|
54 |
|
55 def interpolate(format, *args, **kw): |
|
56 return _interpolate(format, args, kw) |
|
57 |
|
58 def emit(format, *args, **kw): |
|
59 try: |
|
60 f = kw['_file'] |
|
61 except KeyError: |
|
62 f = sys.stdout |
|
63 f.write(_interpolate(format, args, kw)) |
|
64 |
|
65 translate_prog = None |
|
66 |
|
67 def translate(text, pre=0): |
|
68 global translate_prog |
|
69 if not translate_prog: |
|
70 translate_prog = prog = re.compile( |
|
71 r'\b(http|ftp|https)://\S+(\b|/)|\b[-.\w]+@[-.\w]+') |
|
72 else: |
|
73 prog = translate_prog |
|
74 i = 0 |
|
75 list = [] |
|
76 while 1: |
|
77 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">%s</A>' % (url, url) |
|
93 else: |
|
94 repl = url |
|
95 list.append(repl) |
|
96 j = len(text) |
|
97 list.append(escape(text[i:j])) |
|
98 return ''.join(list) |
|
99 |
|
100 def emphasize(line): |
|
101 return re.sub(r'\*([a-zA-Z]+)\*', r'<I>\1</I>', line) |
|
102 |
|
103 revparse_prog = None |
|
104 |
|
105 def revparse(rev): |
|
106 global revparse_prog |
|
107 if not revparse_prog: |
|
108 revparse_prog = re.compile(r'^(\d{1,3})\.(\d{1,4})$') |
|
109 m = revparse_prog.match(rev) |
|
110 if not m: |
|
111 return None |
|
112 [major, minor] = map(int, m.group(1, 2)) |
|
113 return major, minor |
|
114 |
|
115 logon = 0 |
|
116 def log(text): |
|
117 if logon: |
|
118 logfile = open("logfile", "a") |
|
119 logfile.write(text + "\n") |
|
120 logfile.close() |
|
121 |
|
122 def load_cookies(): |
|
123 if not os.environ.has_key('HTTP_COOKIE'): |
|
124 return {} |
|
125 raw = os.environ['HTTP_COOKIE'] |
|
126 words = [s.strip() for s in raw.split(';')] |
|
127 cookies = {} |
|
128 for word in words: |
|
129 i = word.find('=') |
|
130 if i >= 0: |
|
131 key, value = word[:i], word[i+1:] |
|
132 cookies[key] = value |
|
133 return cookies |
|
134 |
|
135 def load_my_cookie(): |
|
136 cookies = load_cookies() |
|
137 try: |
|
138 value = cookies[COOKIE_NAME] |
|
139 except KeyError: |
|
140 return {} |
|
141 import urllib |
|
142 value = urllib.unquote(value) |
|
143 words = value.split('/') |
|
144 while len(words) < 3: |
|
145 words.append('') |
|
146 author = '/'.join(words[:-2]) |
|
147 email = words[-2] |
|
148 password = words[-1] |
|
149 return {'author': author, |
|
150 'email': email, |
|
151 'password': password} |
|
152 |
|
153 def send_my_cookie(ui): |
|
154 name = COOKIE_NAME |
|
155 value = "%s/%s/%s" % (ui.author, ui.email, ui.password) |
|
156 import urllib |
|
157 value = urllib.quote(value) |
|
158 then = now + COOKIE_LIFETIME |
|
159 gmt = time.gmtime(then) |
|
160 path = os.environ.get('SCRIPT_NAME', '/cgi-bin/') |
|
161 print "Set-Cookie: %s=%s; path=%s;" % (name, value, path), |
|
162 print time.strftime("expires=%a, %d-%b-%y %X GMT", gmt) |
|
163 |
|
164 class MagicDict: |
|
165 |
|
166 def __init__(self, d, quote): |
|
167 self.__d = d |
|
168 self.__quote = quote |
|
169 |
|
170 def __getitem__(self, key): |
|
171 for d in self.__d: |
|
172 try: |
|
173 value = d[key] |
|
174 if value: |
|
175 value = str(value) |
|
176 if self.__quote: |
|
177 value = escapeq(value) |
|
178 return value |
|
179 except KeyError: |
|
180 pass |
|
181 return '' |
|
182 |
|
183 class UserInput: |
|
184 |
|
185 def __init__(self): |
|
186 self.__form = cgi.FieldStorage() |
|
187 #log("\n\nbody: " + self.body) |
|
188 |
|
189 def __getattr__(self, name): |
|
190 if name[0] == '_': |
|
191 raise AttributeError |
|
192 try: |
|
193 value = self.__form[name].value |
|
194 except (TypeError, KeyError): |
|
195 value = '' |
|
196 else: |
|
197 value = value.strip() |
|
198 setattr(self, name, value) |
|
199 return value |
|
200 |
|
201 def __getitem__(self, key): |
|
202 return getattr(self, key) |
|
203 |
|
204 class FaqEntry: |
|
205 |
|
206 def __init__(self, fp, file, sec_num): |
|
207 self.file = file |
|
208 self.sec, self.num = sec_num |
|
209 if fp: |
|
210 import rfc822 |
|
211 self.__headers = rfc822.Message(fp) |
|
212 self.body = fp.read().strip() |
|
213 else: |
|
214 self.__headers = {'title': "%d.%d. " % sec_num} |
|
215 self.body = '' |
|
216 |
|
217 def __getattr__(self, name): |
|
218 if name[0] == '_': |
|
219 raise AttributeError |
|
220 key = '-'.join(name.split('_')) |
|
221 try: |
|
222 value = self.__headers[key] |
|
223 except KeyError: |
|
224 value = '' |
|
225 setattr(self, name, value) |
|
226 return value |
|
227 |
|
228 def __getitem__(self, key): |
|
229 return getattr(self, key) |
|
230 |
|
231 def load_version(self): |
|
232 command = interpolate(SH_RLOG_H, self) |
|
233 p = os.popen(command) |
|
234 version = '' |
|
235 while 1: |
|
236 line = p.readline() |
|
237 if not line: |
|
238 break |
|
239 if line[:5] == 'head:': |
|
240 version = line[5:].strip() |
|
241 p.close() |
|
242 self.version = version |
|
243 |
|
244 def getmtime(self): |
|
245 if not self.last_changed_date: |
|
246 return 0 |
|
247 try: |
|
248 return os.stat(self.file)[stat.ST_MTIME] |
|
249 except os.error: |
|
250 return 0 |
|
251 |
|
252 def emit_marks(self): |
|
253 mtime = self.getmtime() |
|
254 if mtime >= now - DT_VERY_RECENT: |
|
255 emit(MARK_VERY_RECENT, self) |
|
256 elif mtime >= now - DT_RECENT: |
|
257 emit(MARK_RECENT, self) |
|
258 |
|
259 def show(self, edit=1): |
|
260 emit(ENTRY_HEADER1, self) |
|
261 self.emit_marks() |
|
262 emit(ENTRY_HEADER2, self) |
|
263 pre = 0 |
|
264 raw = 0 |
|
265 for line in self.body.split('\n'): |
|
266 # Allow the user to insert raw html into a FAQ answer |
|
267 # (Skip Montanaro, with changes by Guido) |
|
268 tag = line.rstrip().lower() |
|
269 if tag == '<html>': |
|
270 raw = 1 |
|
271 continue |
|
272 if tag == '</html>': |
|
273 raw = 0 |
|
274 continue |
|
275 if raw: |
|
276 print line |
|
277 continue |
|
278 if not line.strip(): |
|
279 if pre: |
|
280 print '</PRE>' |
|
281 pre = 0 |
|
282 else: |
|
283 print '<P>' |
|
284 else: |
|
285 if not line[0].isspace(): |
|
286 if pre: |
|
287 print '</PRE>' |
|
288 pre = 0 |
|
289 else: |
|
290 if not pre: |
|
291 print '<PRE>' |
|
292 pre = 1 |
|
293 if '/' in line or '@' in line: |
|
294 line = translate(line, pre) |
|
295 elif '<' in line or '&' in line: |
|
296 line = escape(line) |
|
297 if not pre and '*' in line: |
|
298 line = emphasize(line) |
|
299 print line |
|
300 if pre: |
|
301 print '</PRE>' |
|
302 pre = 0 |
|
303 if edit: |
|
304 print '<P>' |
|
305 emit(ENTRY_FOOTER, self) |
|
306 if self.last_changed_date: |
|
307 emit(ENTRY_LOGINFO, self) |
|
308 print '<P>' |
|
309 |
|
310 class FaqDir: |
|
311 |
|
312 entryclass = FaqEntry |
|
313 |
|
314 __okprog = re.compile(OKFILENAME) |
|
315 |
|
316 def __init__(self, dir=os.curdir): |
|
317 self.__dir = dir |
|
318 self.__files = None |
|
319 |
|
320 def __fill(self): |
|
321 if self.__files is not None: |
|
322 return |
|
323 self.__files = files = [] |
|
324 okprog = self.__okprog |
|
325 for file in os.listdir(self.__dir): |
|
326 if self.__okprog.match(file): |
|
327 files.append(file) |
|
328 files.sort() |
|
329 |
|
330 def good(self, file): |
|
331 return self.__okprog.match(file) |
|
332 |
|
333 def parse(self, file): |
|
334 m = self.good(file) |
|
335 if not m: |
|
336 return None |
|
337 sec, num = m.group(1, 2) |
|
338 return int(sec), int(num) |
|
339 |
|
340 def list(self): |
|
341 # XXX Caller shouldn't modify result |
|
342 self.__fill() |
|
343 return self.__files |
|
344 |
|
345 def open(self, file): |
|
346 sec_num = self.parse(file) |
|
347 if not sec_num: |
|
348 raise InvalidFile(file) |
|
349 try: |
|
350 fp = open(file) |
|
351 except IOError, msg: |
|
352 raise NoSuchFile(file, msg) |
|
353 try: |
|
354 return self.entryclass(fp, file, sec_num) |
|
355 finally: |
|
356 fp.close() |
|
357 |
|
358 def show(self, file, edit=1): |
|
359 self.open(file).show(edit=edit) |
|
360 |
|
361 def new(self, section): |
|
362 if not SECTION_TITLES.has_key(section): |
|
363 raise NoSuchSection(section) |
|
364 maxnum = 0 |
|
365 for file in self.list(): |
|
366 sec, num = self.parse(file) |
|
367 if sec == section: |
|
368 maxnum = max(maxnum, num) |
|
369 sec_num = (section, maxnum+1) |
|
370 file = NEWFILENAME % sec_num |
|
371 return self.entryclass(None, file, sec_num) |
|
372 |
|
373 class FaqWizard: |
|
374 |
|
375 def __init__(self): |
|
376 self.ui = UserInput() |
|
377 self.dir = FaqDir() |
|
378 |
|
379 def go(self): |
|
380 print 'Content-type: text/html' |
|
381 req = self.ui.req or 'home' |
|
382 mname = 'do_%s' % req |
|
383 try: |
|
384 meth = getattr(self, mname) |
|
385 except AttributeError: |
|
386 self.error("Bad request type %r." % (req,)) |
|
387 else: |
|
388 try: |
|
389 meth() |
|
390 except InvalidFile, exc: |
|
391 self.error("Invalid entry file name %s" % exc.file) |
|
392 except NoSuchFile, exc: |
|
393 self.error("No entry with file name %s" % exc.file) |
|
394 except NoSuchSection, exc: |
|
395 self.error("No section number %s" % exc.section) |
|
396 self.epilogue() |
|
397 |
|
398 def error(self, message, **kw): |
|
399 self.prologue(T_ERROR) |
|
400 emit(message, kw) |
|
401 |
|
402 def prologue(self, title, entry=None, **kw): |
|
403 emit(PROLOGUE, entry, kwdict=kw, title=escape(title)) |
|
404 |
|
405 def epilogue(self): |
|
406 emit(EPILOGUE) |
|
407 |
|
408 def do_home(self): |
|
409 self.prologue(T_HOME) |
|
410 emit(HOME) |
|
411 |
|
412 def do_debug(self): |
|
413 self.prologue("FAQ Wizard Debugging") |
|
414 form = cgi.FieldStorage() |
|
415 cgi.print_form(form) |
|
416 cgi.print_environ(os.environ) |
|
417 cgi.print_directory() |
|
418 cgi.print_arguments() |
|
419 |
|
420 def do_search(self): |
|
421 query = self.ui.query |
|
422 if not query: |
|
423 self.error("Empty query string!") |
|
424 return |
|
425 if self.ui.querytype == 'simple': |
|
426 query = re.escape(query) |
|
427 queries = [query] |
|
428 elif self.ui.querytype in ('anykeywords', 'allkeywords'): |
|
429 words = filter(None, re.split('\W+', query)) |
|
430 if not words: |
|
431 self.error("No keywords specified!") |
|
432 return |
|
433 words = map(lambda w: r'\b%s\b' % w, words) |
|
434 if self.ui.querytype[:3] == 'any': |
|
435 queries = ['|'.join(words)] |
|
436 else: |
|
437 # Each of the individual queries must match |
|
438 queries = words |
|
439 else: |
|
440 # Default to regular expression |
|
441 queries = [query] |
|
442 self.prologue(T_SEARCH) |
|
443 progs = [] |
|
444 for query in queries: |
|
445 if self.ui.casefold == 'no': |
|
446 p = re.compile(query) |
|
447 else: |
|
448 p = re.compile(query, re.IGNORECASE) |
|
449 progs.append(p) |
|
450 hits = [] |
|
451 for file in self.dir.list(): |
|
452 try: |
|
453 entry = self.dir.open(file) |
|
454 except FileError: |
|
455 constants |
|
456 for p in progs: |
|
457 if not p.search(entry.title) and not p.search(entry.body): |
|
458 break |
|
459 else: |
|
460 hits.append(file) |
|
461 if not hits: |
|
462 emit(NO_HITS, self.ui, count=0) |
|
463 elif len(hits) <= MAXHITS: |
|
464 if len(hits) == 1: |
|
465 emit(ONE_HIT, count=1) |
|
466 else: |
|
467 emit(FEW_HITS, count=len(hits)) |
|
468 self.format_all(hits, headers=0) |
|
469 else: |
|
470 emit(MANY_HITS, count=len(hits)) |
|
471 self.format_index(hits) |
|
472 |
|
473 def do_all(self): |
|
474 self.prologue(T_ALL) |
|
475 files = self.dir.list() |
|
476 self.last_changed(files) |
|
477 self.format_index(files, localrefs=1) |
|
478 self.format_all(files) |
|
479 |
|
480 def do_compat(self): |
|
481 files = self.dir.list() |
|
482 emit(COMPAT) |
|
483 self.last_changed(files) |
|
484 self.format_index(files, localrefs=1) |
|
485 self.format_all(files, edit=0) |
|
486 sys.exit(0) # XXX Hack to suppress epilogue |
|
487 |
|
488 def last_changed(self, files): |
|
489 latest = 0 |
|
490 for file in files: |
|
491 entry = self.dir.open(file) |
|
492 if entry: |
|
493 mtime = mtime = entry.getmtime() |
|
494 if mtime > latest: |
|
495 latest = mtime |
|
496 print time.strftime(LAST_CHANGED, time.localtime(latest)) |
|
497 emit(EXPLAIN_MARKS) |
|
498 |
|
499 def format_all(self, files, edit=1, headers=1): |
|
500 sec = 0 |
|
501 for file in files: |
|
502 try: |
|
503 entry = self.dir.open(file) |
|
504 except NoSuchFile: |
|
505 continue |
|
506 if headers and entry.sec != sec: |
|
507 sec = entry.sec |
|
508 try: |
|
509 title = SECTION_TITLES[sec] |
|
510 except KeyError: |
|
511 title = "Untitled" |
|
512 emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n", |
|
513 sec=sec, title=title) |
|
514 entry.show(edit=edit) |
|
515 |
|
516 def do_index(self): |
|
517 self.prologue(T_INDEX) |
|
518 files = self.dir.list() |
|
519 self.last_changed(files) |
|
520 self.format_index(files, add=1) |
|
521 |
|
522 def format_index(self, files, add=0, localrefs=0): |
|
523 sec = 0 |
|
524 for file in files: |
|
525 try: |
|
526 entry = self.dir.open(file) |
|
527 except NoSuchFile: |
|
528 continue |
|
529 if entry.sec != sec: |
|
530 if sec: |
|
531 if add: |
|
532 emit(INDEX_ADDSECTION, sec=sec) |
|
533 emit(INDEX_ENDSECTION, sec=sec) |
|
534 sec = entry.sec |
|
535 try: |
|
536 title = SECTION_TITLES[sec] |
|
537 except KeyError: |
|
538 title = "Untitled" |
|
539 emit(INDEX_SECTION, sec=sec, title=title) |
|
540 if localrefs: |
|
541 emit(LOCAL_ENTRY, entry) |
|
542 else: |
|
543 emit(INDEX_ENTRY, entry) |
|
544 entry.emit_marks() |
|
545 if sec: |
|
546 if add: |
|
547 emit(INDEX_ADDSECTION, sec=sec) |
|
548 emit(INDEX_ENDSECTION, sec=sec) |
|
549 |
|
550 def do_recent(self): |
|
551 if not self.ui.days: |
|
552 days = 1 |
|
553 else: |
|
554 days = float(self.ui.days) |
|
555 try: |
|
556 cutoff = now - days * 24 * 3600 |
|
557 except OverflowError: |
|
558 cutoff = 0 |
|
559 list = [] |
|
560 for file in self.dir.list(): |
|
561 entry = self.dir.open(file) |
|
562 if not entry: |
|
563 continue |
|
564 mtime = entry.getmtime() |
|
565 if mtime >= cutoff: |
|
566 list.append((mtime, file)) |
|
567 list.sort() |
|
568 list.reverse() |
|
569 self.prologue(T_RECENT) |
|
570 if days <= 1: |
|
571 period = "%.2g hours" % (days*24) |
|
572 else: |
|
573 period = "%.6g days" % days |
|
574 if not list: |
|
575 emit(NO_RECENT, period=period) |
|
576 elif len(list) == 1: |
|
577 emit(ONE_RECENT, period=period) |
|
578 else: |
|
579 emit(SOME_RECENT, period=period, count=len(list)) |
|
580 self.format_all(map(lambda (mtime, file): file, list), headers=0) |
|
581 emit(TAIL_RECENT) |
|
582 |
|
583 def do_roulette(self): |
|
584 import random |
|
585 files = self.dir.list() |
|
586 if not files: |
|
587 self.error("No entries.") |
|
588 return |
|
589 file = random.choice(files) |
|
590 self.prologue(T_ROULETTE) |
|
591 emit(ROULETTE) |
|
592 self.dir.show(file) |
|
593 |
|
594 def do_help(self): |
|
595 self.prologue(T_HELP) |
|
596 emit(HELP) |
|
597 |
|
598 def do_show(self): |
|
599 entry = self.dir.open(self.ui.file) |
|
600 self.prologue(T_SHOW) |
|
601 entry.show() |
|
602 |
|
603 def do_add(self): |
|
604 self.prologue(T_ADD) |
|
605 emit(ADD_HEAD) |
|
606 sections = SECTION_TITLES.items() |
|
607 sections.sort() |
|
608 for section, title in sections: |
|
609 emit(ADD_SECTION, section=section, title=title) |
|
610 emit(ADD_TAIL) |
|
611 |
|
612 def do_delete(self): |
|
613 self.prologue(T_DELETE) |
|
614 emit(DELETE) |
|
615 |
|
616 def do_log(self): |
|
617 entry = self.dir.open(self.ui.file) |
|
618 self.prologue(T_LOG, entry) |
|
619 emit(LOG, entry) |
|
620 self.rlog(interpolate(SH_RLOG, entry), entry) |
|
621 |
|
622 def rlog(self, command, entry=None): |
|
623 output = os.popen(command).read() |
|
624 sys.stdout.write('<PRE>') |
|
625 athead = 0 |
|
626 lines = output.split('\n') |
|
627 while lines and not lines[-1]: |
|
628 del lines[-1] |
|
629 if lines: |
|
630 line = lines[-1] |
|
631 if line[:1] == '=' and len(line) >= 40 and \ |
|
632 line == line[0]*len(line): |
|
633 del lines[-1] |
|
634 headrev = None |
|
635 for line in lines: |
|
636 if entry and athead and line[:9] == 'revision ': |
|
637 rev = line[9:].split() |
|
638 mami = revparse(rev) |
|
639 if not mami: |
|
640 print line |
|
641 else: |
|
642 emit(REVISIONLINK, entry, rev=rev, line=line) |
|
643 if mami[1] > 1: |
|
644 prev = "%d.%d" % (mami[0], mami[1]-1) |
|
645 emit(DIFFLINK, entry, prev=prev, rev=rev) |
|
646 if headrev: |
|
647 emit(DIFFLINK, entry, prev=rev, rev=headrev) |
|
648 else: |
|
649 headrev = rev |
|
650 print |
|
651 athead = 0 |
|
652 else: |
|
653 athead = 0 |
|
654 if line[:1] == '-' and len(line) >= 20 and \ |
|
655 line == len(line) * line[0]: |
|
656 athead = 1 |
|
657 sys.stdout.write('<HR>') |
|
658 else: |
|
659 print line |
|
660 print '</PRE>' |
|
661 |
|
662 def do_revision(self): |
|
663 entry = self.dir.open(self.ui.file) |
|
664 rev = self.ui.rev |
|
665 mami = revparse(rev) |
|
666 if not mami: |
|
667 self.error("Invalid revision number: %r." % (rev,)) |
|
668 self.prologue(T_REVISION, entry) |
|
669 self.shell(interpolate(SH_REVISION, entry, rev=rev)) |
|
670 |
|
671 def do_diff(self): |
|
672 entry = self.dir.open(self.ui.file) |
|
673 prev = self.ui.prev |
|
674 rev = self.ui.rev |
|
675 mami = revparse(rev) |
|
676 if not mami: |
|
677 self.error("Invalid revision number: %r." % (rev,)) |
|
678 if prev: |
|
679 if not revparse(prev): |
|
680 self.error("Invalid previous revision number: %r." % (prev,)) |
|
681 else: |
|
682 prev = '%d.%d' % (mami[0], mami[1]) |
|
683 self.prologue(T_DIFF, entry) |
|
684 self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev)) |
|
685 |
|
686 def shell(self, command): |
|
687 output = os.popen(command).read() |
|
688 sys.stdout.write('<PRE>') |
|
689 print escape(output) |
|
690 print '</PRE>' |
|
691 |
|
692 def do_new(self): |
|
693 entry = self.dir.new(section=int(self.ui.section)) |
|
694 entry.version = '*new*' |
|
695 self.prologue(T_EDIT) |
|
696 emit(EDITHEAD) |
|
697 emit(EDITFORM1, entry, editversion=entry.version) |
|
698 emit(EDITFORM2, entry, load_my_cookie()) |
|
699 emit(EDITFORM3) |
|
700 entry.show(edit=0) |
|
701 |
|
702 def do_edit(self): |
|
703 entry = self.dir.open(self.ui.file) |
|
704 entry.load_version() |
|
705 self.prologue(T_EDIT) |
|
706 emit(EDITHEAD) |
|
707 emit(EDITFORM1, entry, editversion=entry.version) |
|
708 emit(EDITFORM2, entry, load_my_cookie()) |
|
709 emit(EDITFORM3) |
|
710 entry.show(edit=0) |
|
711 |
|
712 def do_review(self): |
|
713 send_my_cookie(self.ui) |
|
714 if self.ui.editversion == '*new*': |
|
715 sec, num = self.dir.parse(self.ui.file) |
|
716 entry = self.dir.new(section=sec) |
|
717 entry.version = "*new*" |
|
718 if entry.file != self.ui.file: |
|
719 self.error("Commit version conflict!") |
|
720 emit(NEWCONFLICT, self.ui, sec=sec, num=num) |
|
721 return |
|
722 else: |
|
723 entry = self.dir.open(self.ui.file) |
|
724 entry.load_version() |
|
725 # Check that the FAQ entry number didn't change |
|
726 if self.ui.title.split()[:1] != entry.title.split()[:1]: |
|
727 self.error("Don't change the entry number please!") |
|
728 return |
|
729 # Check that the edited version is the current version |
|
730 if entry.version != self.ui.editversion: |
|
731 self.error("Commit version conflict!") |
|
732 emit(VERSIONCONFLICT, entry, self.ui) |
|
733 return |
|
734 commit_ok = ((not PASSWORD |
|
735 or self.ui.password == PASSWORD) |
|
736 and self.ui.author |
|
737 and '@' in self.ui.email |
|
738 and self.ui.log) |
|
739 if self.ui.commit: |
|
740 if not commit_ok: |
|
741 self.cantcommit() |
|
742 else: |
|
743 self.commit(entry) |
|
744 return |
|
745 self.prologue(T_REVIEW) |
|
746 emit(REVIEWHEAD) |
|
747 entry.body = self.ui.body |
|
748 entry.title = self.ui.title |
|
749 entry.show(edit=0) |
|
750 emit(EDITFORM1, self.ui, entry) |
|
751 if commit_ok: |
|
752 emit(COMMIT) |
|
753 else: |
|
754 emit(NOCOMMIT_HEAD) |
|
755 self.errordetail() |
|
756 emit(NOCOMMIT_TAIL) |
|
757 emit(EDITFORM2, self.ui, entry, load_my_cookie()) |
|
758 emit(EDITFORM3) |
|
759 |
|
760 def cantcommit(self): |
|
761 self.prologue(T_CANTCOMMIT) |
|
762 print CANTCOMMIT_HEAD |
|
763 self.errordetail() |
|
764 print CANTCOMMIT_TAIL |
|
765 |
|
766 def errordetail(self): |
|
767 if PASSWORD and self.ui.password != PASSWORD: |
|
768 emit(NEED_PASSWD) |
|
769 if not self.ui.log: |
|
770 emit(NEED_LOG) |
|
771 if not self.ui.author: |
|
772 emit(NEED_AUTHOR) |
|
773 if not self.ui.email: |
|
774 emit(NEED_EMAIL) |
|
775 |
|
776 def commit(self, entry): |
|
777 file = entry.file |
|
778 # Normalize line endings in body |
|
779 if '\r' in self.ui.body: |
|
780 self.ui.body = re.sub('\r\n?', '\n', self.ui.body) |
|
781 # Normalize whitespace in title |
|
782 self.ui.title = ' '.join(self.ui.title.split()) |
|
783 # Check that there were any changes |
|
784 if self.ui.body == entry.body and self.ui.title == entry.title: |
|
785 self.error("You didn't make any changes!") |
|
786 return |
|
787 |
|
788 # need to lock here because otherwise the file exists and is not writable (on NT) |
|
789 command = interpolate(SH_LOCK, file=file) |
|
790 p = os.popen(command) |
|
791 output = p.read() |
|
792 |
|
793 try: |
|
794 os.unlink(file) |
|
795 except os.error: |
|
796 pass |
|
797 try: |
|
798 f = open(file, 'w') |
|
799 except IOError, why: |
|
800 self.error(CANTWRITE, file=file, why=why) |
|
801 return |
|
802 date = time.ctime(now) |
|
803 emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0) |
|
804 f.write('\n') |
|
805 f.write(self.ui.body) |
|
806 f.write('\n') |
|
807 f.close() |
|
808 |
|
809 import tempfile |
|
810 tf = tempfile.NamedTemporaryFile() |
|
811 emit(LOGHEADER, self.ui, os.environ, date=date, _file=tf) |
|
812 tf.flush() |
|
813 tf.seek(0) |
|
814 |
|
815 command = interpolate(SH_CHECKIN, file=file, tfn=tf.name) |
|
816 log("\n\n" + command) |
|
817 p = os.popen(command) |
|
818 output = p.read() |
|
819 sts = p.close() |
|
820 log("output: " + output) |
|
821 log("done: " + str(sts)) |
|
822 log("TempFile:\n" + tf.read() + "end") |
|
823 |
|
824 if not sts: |
|
825 self.prologue(T_COMMITTED) |
|
826 emit(COMMITTED) |
|
827 else: |
|
828 self.error(T_COMMITFAILED) |
|
829 emit(COMMITFAILED, sts=sts) |
|
830 print '<PRE>%s</PRE>' % escape(output) |
|
831 |
|
832 try: |
|
833 os.unlink(tf.name) |
|
834 except os.error: |
|
835 pass |
|
836 |
|
837 entry = self.dir.open(file) |
|
838 entry.show() |
|
839 |
|
840 wiz = FaqWizard() |
|
841 wiz.go() |