"""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
- next/prev/index links in do_show?
- customize rcs command pathnames
- explanation of editing somewhere
- various embellishments, GIFs, crosslinks, hints, etc.
- create new sections
- rearrange entries
- delete entries
- send email on changes
- optional staging of entries until reviewed?
- freeze entries
- username/password for editors
- Change references to other Q's and whole sections
- support adding annotations, too
- make it more generic (so you can create your own FAQ)
- more OO structure, e.g. add a class representing one FAQ entry
"""
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']
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 (alpha) Front Page")
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 """
Add new entry (at this point)
""" % section
section = nsec
if SECTIONS.has_key(section):
stitle = SECTIONS[section]
else:
stitle = ""
print "
Section %s. %s
" % (section, stitle)
print "
"
print '
%s' % (
name, cgi.escape(title))
if section:
print """
""" % section
else:
print "No FAQ entries?!?!"
def do_show(self):
self.prologue("Python FAQ Entry")
print ""
name = self.name
headers, text = self.read(name)
if not headers:
self.error("Invalid file name", name)
return
self.show(name, headers['title'], text)
def do_all(self):
self.prologue("The Whole Python FAQ")
print ""
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:
section = nsec
if SECTIONS.has_key(section):
stitle = SECTIONS[section]
else:
stitle = ""
print "
Section %s. %s
" % (section, stitle)
print ""
self.show(name, title, text, edit=(self.edit != 'no'))
if not section:
print "No FAQ entries?!?!"
def do_roulette(self):
import whrandom
self.prologue("Python FAQ Roulette")
print """
Please check the correctness of the entry below.
If you find any problems, please edit the entry.
"""
names = os.listdir(os.curdir)
while names:
name = whrandom.choice(names)
headers, text = self.read(name)
if headers:
self.show(name, headers['title'], text)
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 "
"
n = 0
for (mtime, name) in list:
headers, text = self.read(name)
if headers and headers.has_key('last-changed-date'):
self.show(name, headers['title'], text)
n = n+1
if not n:
print "No FAQ entries?!?!"
def do_query(self):
query = self.query
if not query:
self.error("No query string")
return
import regex
self.prologue("Python FAQ Query Results")
p = regex.compile(query, regex.casefold)
names = os.listdir(os.curdir)
names.sort()
print ""
n = 0
for name in names:
headers, text = self.read(name)
if headers:
title = headers['title']
if p.search(title) >= 0 or p.search(text) >= 0:
self.show(name, title, text)
n = n+1
if not n:
print "No hits."
def do_add(self):
section = self.section
if not section:
self.prologue("How to add a new FAQ entry")
print """
Go to the FAQ index
and click on the "Add new entry" link at the end
of the section to which you want to add the entry.
"""
return
try:
nsec = string.atoi(section)
except ValueError:
print "Bad section number", nsec
names = os.listdir(os.curdir)
max = 0
import regex
prog = regex.compile(NAMEREG)
for name in names:
if prog.match(name) >= 0:
s1, s2 = prog.group(1, 2)
n1, n2 = string.atoi(s1), string.atoi(s2)
if n1 == nsec:
if n2 > max:
max = n2
if not max:
self.error("Can't add new sections yet.")
return
num = max+1
name = "faq%02d.%03d.htp" % (nsec, num)
self.name = name
self.add = "yes"
self.number = str(num)
self.do_edit()
def do_delete(self):
self.prologue("How to delete a FAQ entry")
print """
At the moment, there's no direct way to delete entries.
This is because the entry numbers are also their
unique identifiers -- it's a bad idea to renumber entries.
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 Form")
title = headers['title']
version = self.getversion(name)
print "
""" % (name, version)
self.show(name, title, text, edit=0)
def do_review(self):
if self.commit:
self.checkin()
return
name = self.name
text = self.text
title = self.title
headers, oldtext = self.read(name)
if not headers:
self.error("Invalid file name", name)
return
if self.author and '@' in self.email:
self.set_cookie(self.author, self.email)
self.prologue("Python FAQ Review Form")
print ""
self.show(name, title, text, edit=0)
print "
""" % (name, self.version)
def do_info(self):
name = self.name
headers, text = self.read(name)
if not headers:
self.error("Invalid file name", name)
return
self.prologue("Info for %s" % name)
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 '
'
def checkin(self):
import regsub, time, tempfile
name = self.name
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),
'Reload.' % name)
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()
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)
self.prologue("Python FAQ Entry Edited")
print ""
self.show(name, title, text)
if output:
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)
print ''
print 'Reload this entry.' % name
def set_cookie(self, author, email):
name = "Python-FAQ-ID"
value = "%s;%s" % (author, email)
import urllib
value = urllib.quote(value)
print "Set-Cookie: %s=%s; path=/cgi-bin/;" % (name, value),
print "domain=%s;" % os.environ['HTTP_HOST'],
print "expires=Sat, 01-Jan-2000 00:00:00 GMT"
def get_cookie(self):
if not os.environ.has_key('HTTP_COOKIE'):
return "", ""
raw = os.environ['HTTP_COOKIE']
words = 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-ID'):
return "", ""
value = cookies['Python-FAQ-ID']
import urllib
value = urllib.unquote(value)
i = string.rfind(value, ';')
author, email = value[:i], value[i+1:]
return author, email
def showedit(self, name, title, text):
author = self.author
email = self.email
if not author or not email:
a, e = self.get_cookie()
author = author or a
email = email or e
print """
Title:
Please provide the following information for logging purposes:
Name : Email:
Log message (reason for the change):
""" % (author, email, self.log)
def showheaders(self, headers):
print "
"
keys = map(string.lower, headers.keys())
keys.sort()
for key in keys:
print "
%s: %s" % (string.capwords(key, '-'),
headers[key] or '')
print "
"
headers = None
def read(self, name):
self.headers = None
import fnmatch, rfc822
if not fnmatch.fnmatch(name, NAMEPAT):
return None, None
if self.add:
try:
fname = "faq%02d.%03d.htp" % (string.atoi(self.section),
string.atoi(self.number))
except ValueError:
return None, None
if fname != name:
return None, None
headers = {'title': "%s.%s. " % (self.section, self.number)}
text = ""
else:
f = open(name)
headers = rfc822.Message(f)
text = f.read()
f.close()
self.headers = headers
return headers, text
def show(self, name, title, text, edit=1):
# XXX Should put tags around recognizable URLs
# XXX Should also turn "see section N" into hyperlinks
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 "
"
def getversion(self, name):
p = os.popen("/depot/gnu/plat/bin/rlog -h %s &1" % name)
head = "*new*"
while 1:
line = p.readline()
if not line:
break
if line[:5] == 'head:':
head = string.strip(line[5:])
p.close()
return head
def prologue(self, title):
title = cgi.escape(title)
print '''
%s
%s
''' % (title, title)
def error(self, *messages):
self.prologue("Python FAQ error")
print "Sorry, an error occurred: "
for message in messages:
print message,
print
def epilogue(self):
print '''
Python home /
FAQ home /
Feedback to GvR
'''
translate_prog = None
def translate(self, text):
if not self.translate_prog:
import regex
url = '\(http\|ftp\)://[^ \t\r\n]*'
email = '\<[-a-zA-Z0-9._]+@[-a-zA-Z0-9._]+'
self.translate_prog = prog = regex.compile(url + "\|" + email)
else:
prog = self.translate_prog
i = 0
list = []
while 1:
j = prog.search(text, i)
if j < 0:
break
list.append(cgi.escape(text[i:j]))
i = j
url = prog.group(0)
while url[-1] in ");:,.?":
url = url[:-1]
url = cgi.escape(url)
if ':' in url:
repl = '%s' % (url, url)
else:
repl = '<%s>' % (url, url)
list.append(repl)
i = i + len(url)
j = len(text)
list.append(cgi.escape(text[i:j]))
return string.join(list, '')
print "Content-type: text/html"
dt = 0
try:
import time
t1 = time.time()
import cgi, string, os, sys
x = FAQServer()
x.main()
t2 = time.time()
dt = t2-t1
except:
print "\nSorry, an error occurred"
cgi.print_exception()
print "
(running time = %s seconds)" % str(round(dt, 3))
# The following bootstrap script must be placed in cgi-bin/faq.py:
BOOTSTRAP = """
#! /usr/local/bin/python
FAQDIR = "/usr/people/guido/python/FAQ"
import os, sys
os.chdir(FAQDIR)
sys.path.insert(0, os.curdir)
import faqmain
"""