"""Interactive FAQ project. Note that this is not an executable script; it's an importable module. The actual CGI script can be kept minimal; it's appended at the end of this file as a string constant. XXX TO DO XXX User Features TO DO - next/prev/index links in do_show??? - explanation of editing somewhere - embellishments, GIFs, hints, etc. - support adding annotations, too - restrict recent changes to last week (or make it an option) - extended search capabilities XXX Management Features TO DO - username/password for authors - create new sections - rearrange entries - delete entries - freeze entries - send email on changes? - send email on ERRORS! - optional staging of entries until reviewed? (could be done using rcs branches!) - prevent race conditions on nearly simultaneous commits XXX Performance - could cache generated HTML - could speed up searches with a separate index file XXX Code organization TO DO - read section titles from a file (could be a Python file: import faqcustom) - customize rcs command pathnames (and everything else) - make it more generic (so you can create your own FAQ) - more OO structure, e.g. add a class representing one FAQ entry """ # NB for timing purposes, the imports are at the end of this file PASSWORD = "Spam" NAMEPAT = "faq??.???.htp" NAMEREG = "^faq\([0-9][0-9]\)\.\([0-9][0-9][0-9]\)\.htp$" SECTIONS = { "1": "General information and availability", "2": "Python in the real world", "3": "Building Python and Other Known Bugs", "4": "Programming in Python", "5": "Extending Python", "6": "Python's design", "7": "Using Python on non-UNIX platforms", } class FAQServer: def __init__(self): pass def main(self): self.form = cgi.FieldStorage() req = self.req or 'frontpage' try: method = getattr(self, 'do_%s' % req) except AttributeError: print "Unrecognized request type", req else: method() self.epilogue() KEYS = ['req', 'query', 'name', 'text', 'commit', 'title', 'author', 'email', 'log', 'section', 'number', 'add', 'version', 'edit', 'password'] def __getattr__(self, key): if key not in self.KEYS: raise AttributeError try: form = self.form try: item = form[key] except TypeError, msg: raise KeyError, msg, sys.exc_traceback except KeyError: return '' value = self.form[key].value value = string.strip(value) setattr(self, key, value) return value def do_frontpage(self): self.prologue("Python FAQ Wizard (beta test)") print """
Disclaimer: these pages are intended to be edited by anyone. Please exercise discretion when editing, don't be rude, etc. """ def do_index(self): self.prologue("Python FAQ Index") names = os.listdir(os.curdir) names.sort() section = None for name in names: headers, text = self.read(name) if headers: title = headers['title'] i = string.find(title, '.') nsec = title[:i] if nsec != section: if section: print """
Use `Reload' to show another one." break else: names.remove(name) else: print "No FAQ entries?!?!" def do_recent(self): import fnmatch, stat names = os.listdir(os.curdir) now = time.time() list = [] for name in names: if not fnmatch.fnmatch(name, NAMEPAT): continue try: st = os.stat(name) except os.error: continue tuple = (st[stat.ST_MTIME], name) list.append(tuple) list.sort() list.reverse() self.prologue("Python FAQ, Most Recently Modified First") print "
If you really think an entry needs to be deleted, change the title to "(deleted)" and make the body empty (keep the entry number in the title though). """ def do_edit(self): name = self.name headers, text = self.read(name) if not headers: self.error("Invalid file name", name) return self.prologue("Python FAQ Edit Wizard - Edit Form") print 'Click for Help' title = headers['title'] version = self.getversion(name) print "
' p = os.popen("/depot/gnu/plat/bin/rlog -r %s &1" % self.name) output = p.read() p.close() print cgi.escape(output) print '' print 'View full rcs log' % name def do_rlog(self): name = self.name headers, text = self.read(name) if not headers: self.error("Invalid file name", name) return self.prologue("RCS log for %s" % name) print '
' p = os.popen("/depot/gnu/plat/bin/rlog %s &1" % self.name) output = p.read() p.close() print cgi.escape(output) print '' def checkin(self): import regsub, time, tempfile name = self.name password = self.password if password != PASSWORD: self.error("Invalid password.") return if not (self.log and self.author and '@' in self.email): self.error("No log message, no author, or invalid email.") return headers, oldtext = self.read(name) if not headers: self.error("Invalid file name", name) return version = self.version curversion = self.getversion(name) if version != curversion: self.error( "Version conflict.", "You edited version %s but current version is %s." % ( version, curversion), """
The two most common causes of this problem are:
""", 'Click here to reload the entry and try again.') return text = self.text title = self.title author = self.author email = self.email log = self.log text = regsub.gsub("\r\n", "\n", text) log = regsub.gsub("\r\n", "\n", log) author = string.join(string.split(author)) email = string.join(string.split(email)) title = string.join(string.split(title)) oldtitle = headers['title'] oldtitle = string.join(string.split(oldtitle)) text = string.strip(text) oldtext = string.strip(oldtext) if text == oldtext and title == oldtitle: self.error("No changes.") return # Check that the FAQ entry number didn't change if string.split(title)[:1] != string.split(oldtitle)[:1]: self.error("Don't change the FAQ entry number please.") return remhost = os.environ["REMOTE_HOST"] remaddr = os.environ["REMOTE_ADDR"] try: os.unlink(name + "~") except os.error: pass try: os.rename(name, name + "~") except os.error: pass try: os.unlink(name) except os.error: pass try: f = open(name, "w") except IOError, msg: self.error("Can't open", name, "for writing:", cgi.escape(str(msg))) return now = time.ctime(time.time()) f.write("Title: %s\n" % title) f.write("Last-Changed-Date: %s\n" % now) f.write("Last-Changed-Author: %s\n" % author) f.write("Last-Changed-Email: %s\n" % email) f.write("Last-Changed-Remote-Host: %s\n" % remhost) f.write("Last-Changed-Remote-Address: %s\n" % remaddr) keys = headers.keys() keys.sort() keys.remove('title') for key in keys: if key[:13] != 'last-changed-': f.write("%s: %s\n" % (string.capwords(key, '-'), headers[key])) f.write("\n") f.write(text) f.write("\n") f.close() tfn = tempfile.mktemp() f = open(tfn, "w") f.write("Last-Changed-Date: %s\n" % now) f.write("Last-Changed-Author: %s\n" % author) f.write("Last-Changed-Email: %s\n" % email) f.write("Last-Changed-Remote-Host: %s\n" % remhost) f.write("Last-Changed-Remote-Address: %s\n" % remaddr) f.write("\n") f.write(log) f.write("\n") f.close() # Do this for show() below self.headers = { 'title': title, 'last-changed-date': now, 'last-changed-author': author, 'last-changed-email': email, 'last-changed-remote-host': remhost, 'last-changed-remote-address': remaddr, } p = os.popen(""" /depot/gnu/plat/bin/rcs -l %s &1 /depot/gnu/plat/bin/ci -u %s <%s 2>&1 rm -f %s """ % (name, name, tfn, tfn)) output = p.read() sts = p.close() if not sts: self.set_cookie(author, email, password) self.prologue("Python FAQ Entry Edited") print "
%s" % cgi.escape(output) else: self.error("Python FAQ Entry Commit Failed", "Exit status 0x%04x" % sts) if output: print "
%s" % cgi.escape(output) def set_cookie(self, author, email, password): name = "Python-FAQ-Wizard" value = "%s/%s/%s" % (author, email, password) import urllib value = urllib.quote(value) print "Set-Cookie: %s=%s; path=/cgi-bin/;" % (name, value), import time now = time.time() then = now + 28 * 24 * 3600 gmt = time.gmtime(then) print time.strftime("expires=%a, %d-%b-%x %X GMT", gmt) def get_cookie(self): if not os.environ.has_key('HTTP_COOKIE'): return "", "", "" raw = os.environ['HTTP_COOKIE'] words = map(string.strip, string.split(raw, ';')) cookies = {} for word in words: i = string.find(word, '=') if i >= 0: key, value = word[:i], word[i+1:] cookies[key] = value if not cookies.has_key('Python-FAQ-Wizard'): return "", "", "" value = cookies['Python-FAQ-Wizard'] import urllib value = urllib.unquote(value) words = string.split(value, '/') while len(words) < 3: words.append('') author = string.join(words[:-2], '/') email = words[-2] password = words[-1] return author, email, password def showedit(self, name, title, text): author = self.author email = self.email password = self.password if not author or not email or not password: a, e, p = self.get_cookie() author = author or a email = email or e password = password or p print """ Title:
Name: | |
Email: | |
Password: |
' else: if line[0] not in string.whitespace: if pre: print '' pre = 0 else: if not pre: print '
' pre = 1 if '/' in line or '@' in line: line = self.translate(line) elif '<' in line or '&' in line: line = cgi.escape(line) if not pre and '*' in line: line = self.emphasize(line) print line if pre: print '' pre = 0 print '
' if edit: print """ Edit this entry / Log info """ % (name, name) if self.headers: try: date = self.headers['last-changed-date'] author = self.headers['last-changed-author'] email = self.headers['last-changed-email'] except KeyError: pass else: s = '/ Last changed on %s by %s' print s % (date, email, author) print '
' print "