859 lines
22 KiB
Python
859 lines
22 KiB
Python
"""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 """
|
|
<UL>
|
|
<LI><A HREF="faq.py?req=index">FAQ index</A>
|
|
<LI><A HREF="faq.py?req=all">The whole FAQ</A>
|
|
<LI><A HREF="faq.py?req=roulette">FAQ roulette</A>
|
|
<LI><A HREF="faq.py?req=recent">Recently changed FAQ entries</A>
|
|
<LI><A HREF="faq.py?req=add">Add a new FAQ entry</A>
|
|
<LI><A HREF="faq.py?req=delete">Delete a FAQ entry</A>
|
|
</UL>
|
|
|
|
<HR>
|
|
|
|
<H2>Search the FAQ</H2>
|
|
|
|
<FORM ACTION="faq.py">
|
|
<INPUT TYPE=text NAME=query>
|
|
<INPUT TYPE=submit VALUE="Search"><BR>
|
|
(Case insensitive regular expressions.)
|
|
<INPUT TYPE=hidden NAME=req VALUE=query>
|
|
</FORM>
|
|
<HR>
|
|
<P>
|
|
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 """
|
|
<P>
|
|
<LI><A HREF="faq.py?req=add&section=%s"
|
|
>Add new entry</A> (at this point)
|
|
</UL>
|
|
""" % section
|
|
section = nsec
|
|
if SECTIONS.has_key(section):
|
|
stitle = SECTIONS[section]
|
|
else:
|
|
stitle = ""
|
|
print "<H2>Section %s. %s</H2>" % (section, stitle)
|
|
print "<UL>"
|
|
print '<LI><A HREF="faq.py?req=show&name=%s">%s</A>' % (
|
|
name, cgi.escape(title))
|
|
if section:
|
|
print """
|
|
<P>
|
|
<LI><A HREF="faq.py?req=add&section=%s">Add new entry</A>
|
|
(at this point)
|
|
</UL>
|
|
""" % section
|
|
else:
|
|
print "No FAQ entries?!?!"
|
|
|
|
def do_show(self):
|
|
self.prologue("Python FAQ Entry")
|
|
print "<HR>"
|
|
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):
|
|
import fnmatch, stat
|
|
self.prologue("The Whole Python FAQ")
|
|
names = os.listdir(os.curdir)
|
|
lastmtime = 0
|
|
for name in names:
|
|
if not fnmatch.fnmatch(name, NAMEPAT):
|
|
continue
|
|
try:
|
|
st = os.stat(name)
|
|
except os.error:
|
|
continue
|
|
lastmtime = max(lastmtime, st[stat.ST_MTIME])
|
|
if lastmtime:
|
|
print time.strftime("Last changed on %c %Z",
|
|
time.localtime(lastmtime))
|
|
names.sort()
|
|
section = None
|
|
print "<HR>"
|
|
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 "<H1>Section %s. %s</H1>" % (section, stitle)
|
|
print "<HR>"
|
|
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.
|
|
<P>
|
|
<HR>
|
|
"""
|
|
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 "<P>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 "<HR>"
|
|
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 "<HR>"
|
|
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 <A HREF="faq.py?req=index">FAQ index</A>
|
|
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.
|
|
<P>
|
|
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 '<A HREF="/python/faqhelp.html">Click for Help</A>'
|
|
title = headers['title']
|
|
version = self.getversion(name)
|
|
print "<FORM METHOD=POST ACTION=faq.py>"
|
|
self.showedit(name, title, text)
|
|
if self.add:
|
|
print """
|
|
<INPUT TYPE=hidden NAME=add VALUE=%s>
|
|
<INPUT TYPE=hidden NAME=section VALUE=%s>
|
|
<INPUT TYPE=hidden NAME=number VALUE=%s>
|
|
""" % (self.add, self.section, self.number)
|
|
print """
|
|
<INPUT TYPE=submit VALUE="Review Edit">
|
|
<INPUT TYPE=hidden NAME=req VALUE=review>
|
|
<INPUT TYPE=hidden NAME=name VALUE=%s>
|
|
<INPUT TYPE=hidden NAME=version VALUE=%s>
|
|
</FORM>
|
|
<HR>
|
|
""" % (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 or '@' in self.email or self.password:
|
|
self.set_cookie(self.author, self.email, self.password)
|
|
self.prologue("Python FAQ Edit Wizard - Review Form")
|
|
print '<A HREF="/python/faqhelp.html">Click for Help</A>'
|
|
print "<HR>"
|
|
self.show(name, title, text, edit=0)
|
|
print "<FORM METHOD=POST ACTION=faq.py>"
|
|
if self.password == PASSWORD \
|
|
and self.log and self.author and '@' in self.email:
|
|
print """
|
|
<INPUT TYPE=submit NAME=commit VALUE="Commit">
|
|
Click this button to commit the change.
|
|
<P>
|
|
<HR>
|
|
<P>
|
|
"""
|
|
else:
|
|
print """
|
|
To commit this change, please enter a log message,
|
|
your name, your email address,
|
|
and the correct password in the form below.
|
|
<P>
|
|
<HR>
|
|
<P>
|
|
"""
|
|
self.showedit(name, title, text)
|
|
if self.add:
|
|
print """
|
|
<INPUT TYPE=hidden NAME=add VALUE=%s>
|
|
<INPUT TYPE=hidden NAME=section VALUE=%s>
|
|
<INPUT TYPE=hidden NAME=number VALUE=%s>
|
|
""" % (self.add, self.section, self.number)
|
|
print """
|
|
<BR>
|
|
<INPUT TYPE=submit VALUE="Review Edit">
|
|
<INPUT TYPE=hidden NAME=req VALUE=review>
|
|
<INPUT TYPE=hidden NAME=name VALUE=%s>
|
|
<INPUT TYPE=hidden NAME=version VALUE=%s>
|
|
</FORM>
|
|
<HR>
|
|
""" % (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 '<PRE>'
|
|
p = os.popen("/depot/gnu/plat/bin/rlog -r %s </dev/null 2>&1" %
|
|
self.name)
|
|
output = p.read()
|
|
p.close()
|
|
print cgi.escape(output)
|
|
print '</PRE>'
|
|
print '<A HREF="faq.py?req=rlog&name=%s">View full rcs log</A>' % 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 '<PRE>'
|
|
p = os.popen("/depot/gnu/plat/bin/rlog %s </dev/null 2>&1" % self.name)
|
|
output = p.read()
|
|
p.close()
|
|
print cgi.escape(output)
|
|
print '</PRE>'
|
|
|
|
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),
|
|
"""
|
|
<P>
|
|
The two most common causes of this problem are:
|
|
<UL>
|
|
<LI>After committing a change, you went back in your browser,
|
|
edited the entry some more, and clicked Commit again.
|
|
<LI>Someone else started editing the same entry and committed
|
|
before you did.
|
|
</UL>
|
|
<P>
|
|
""",
|
|
'<A HREF="faq.py?req=show&name=%s"' % name,
|
|
'>Click here to reload the entry and try again.</A>')
|
|
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 </dev/null 2>&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 "<HR>"
|
|
self.show(name, title, text)
|
|
if output:
|
|
print "<PRE>%s</PRE>" % cgi.escape(output)
|
|
else:
|
|
self.error("Python FAQ Entry Commit Failed",
|
|
"Exit status 0x%04x" % sts)
|
|
if output:
|
|
print "<PRE>%s</PRE>" % 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: <INPUT TYPE=text SIZE=70 NAME=title VALUE="%s"><BR>
|
|
<TEXTAREA COLS=80 ROWS=20 NAME=text>%s\n</TEXTAREA>""" % (
|
|
self.escape(title), cgi.escape(string.strip(text)))
|
|
print """<BR>
|
|
Log message (reason for the change):<BR>
|
|
<TEXTAREA COLS=80 ROWS=5 NAME=log>%s\n</TEXTAREA><BR>
|
|
Please provide the following information for logging purposes:
|
|
<TABLE FRAME=none COLS=2>
|
|
<TR>
|
|
<TD>Name:
|
|
<TD><INPUT TYPE=text SIZE=40 NAME=author VALUE="%s">
|
|
<TR>
|
|
<TD>Email:
|
|
<TD><INPUT TYPE=text SIZE=40 NAME=email VALUE="%s">
|
|
<TR>
|
|
<TD>Password:
|
|
<TD><INPUT TYPE=password SIZE=40 NAME=password VALUE="%s">
|
|
</TABLE>
|
|
""" % (self.escape(self.log), self.escape(author),
|
|
self.escape(email), self.escape(password))
|
|
|
|
def escape(self, s):
|
|
import regsub
|
|
if '&' in s:
|
|
s = regsub.gsub("&", "&", s) # Must be done first!
|
|
if '<' in s:
|
|
s = regsub.gsub("<", "<", s)
|
|
if '>' in s:
|
|
s = regsub.gsub(">", ">", s)
|
|
if '"' in s:
|
|
s = regsub.gsub('"', """, s)
|
|
return s
|
|
|
|
def showheaders(self, headers):
|
|
print "<UL>"
|
|
keys = map(string.lower, headers.keys())
|
|
keys.sort()
|
|
for key in keys:
|
|
print "<LI><B>%s:</B> %s" % (string.capwords(key, '-'),
|
|
headers[key] or '')
|
|
print "</UL>"
|
|
|
|
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):
|
|
print "<H2>%s</H2>" % cgi.escape(title)
|
|
pre = 0
|
|
for line in string.split(text, '\n'):
|
|
if not string.strip(line):
|
|
if pre:
|
|
print '</PRE>'
|
|
pre = 0
|
|
else:
|
|
print '<P>'
|
|
else:
|
|
if line[0] not in string.whitespace:
|
|
if pre:
|
|
print '</PRE>'
|
|
pre = 0
|
|
else:
|
|
if not pre:
|
|
print '<PRE>'
|
|
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>'
|
|
pre = 0
|
|
print '<P>'
|
|
if edit:
|
|
print """
|
|
<A HREF="faq.py?req=edit&name=%s">Edit this entry</A> /
|
|
<A HREF="faq.py?req=info&name=%s" TARGET=rlog>Log info</A>
|
|
""" % (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 <A HREF="mailto:%s">%s</A>'
|
|
print s % (date, email, author)
|
|
print '<P>'
|
|
print "<HR>"
|
|
|
|
def getversion(self, name):
|
|
p = os.popen("/depot/gnu/plat/bin/rlog -h %s </dev/null 2>&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 '''
|
|
<HTML>
|
|
<HEAD>
|
|
<TITLE>%s</TITLE>
|
|
</HEAD>
|
|
<BODY BACKGROUND="http://www.python.org/pics/RedShort.gif"
|
|
BGCOLOR="#FFFFFF"
|
|
TEXT="#000000"
|
|
LINK="#AA0000"
|
|
VLINK="#906A6A">
|
|
<H1>%s</H1>
|
|
''' % (title, title)
|
|
|
|
def error(self, *messages):
|
|
self.prologue("Python FAQ error")
|
|
print "Sorry, an error occurred:<BR>"
|
|
for message in messages:
|
|
print message,
|
|
print
|
|
|
|
def epilogue(self):
|
|
if self.edit == 'no':
|
|
global wanttime
|
|
wanttime = 0
|
|
else:
|
|
print '''
|
|
<P>
|
|
<HR>
|
|
<A HREF="http://www.python.org">Python home</A> /
|
|
<A HREF="faq.py?req=frontpage">FAQ Wizard home</A> /
|
|
Feedback to <A HREF="mailto:guido@python.org">GvR</A>
|
|
'''
|
|
print '''
|
|
</BODY>
|
|
</HTML>
|
|
'''
|
|
|
|
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 = self.escape(url)
|
|
if ':' in url:
|
|
repl = '<A HREF="%s">%s</A>' % (url, url)
|
|
else:
|
|
repl = '<A HREF="mailto:%s"><%s></A>' % (url, url)
|
|
list.append(repl)
|
|
i = i + len(url)
|
|
j = len(text)
|
|
list.append(cgi.escape(text[i:j]))
|
|
return string.join(list, '')
|
|
|
|
emphasize_prog = None
|
|
|
|
def emphasize(self, line):
|
|
import regsub
|
|
if not self.emphasize_prog:
|
|
import regex
|
|
pat = "\*\([a-zA-Z]+\)\*"
|
|
self.emphasize_prog = prog = regex.compile(pat)
|
|
else:
|
|
prog = self.emphasize_prog
|
|
return regsub.gsub(prog, "<I>\\1</I>", line)
|
|
|
|
print "Content-type: text/html"
|
|
dt = 0
|
|
wanttime = 0
|
|
try:
|
|
import time
|
|
t1 = time.time()
|
|
import cgi, string, os, sys
|
|
x = FAQServer()
|
|
x.main()
|
|
t2 = time.time()
|
|
dt = t2-t1
|
|
wanttime = 1
|
|
except:
|
|
print "\n<HR>Sorry, an error occurred"
|
|
cgi.print_exception()
|
|
if wanttime:
|
|
print "<BR>(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
|
|
"""
|