I'm happy with this.

This commit is contained in:
Guido van Rossum 1997-05-26 05:43:29 +00:00
parent 1677e5b5dd
commit ea31ea2859
2 changed files with 478 additions and 291 deletions

View file

@ -1,44 +1,13 @@
# Miscellaneous customization constants """FAQ Wizard customization module.
PASSWORD = "Spam" # Edit password. Change this!
FAQCGI = 'faqw.py' # Relative URL of the FAQ cgi script
FAQNAME = "Python FAQ" # Name of the FAQ
OWNERNAME = "GvR" # Name for feedback
OWNEREMAIL = "guido@python.org" # Email for feedback
HOMEURL = "http://www.python.org" # Related home page
HOMENAME = "Python home" # Name of related home page
MAXHITS = 10 # Max #hits to be shown directly
COOKIE_NAME = "Python-FAQ-Wizard" # Name used for Netscape cookie
COOKIE_LIFETIME = 4 *7 * 24 * 3600 # Cookie expiration in seconds
# RCS commands Edit this file to customize the FAQ Wizard. For normal purposes, you
RCSBINDIR = "/depot/gnu/plat/bin/" # Directory containing RCS commands should only have to change the FAQ section titles and the small group
SH_RLOG = RCSBINDIR + "rlog %(file)s </dev/null 2>&1" of parameters below it.
SH_RLOG_H = RCSBINDIR + "rlog -h %(file)s </dev/null 2>&1"
SH_RDIFF = RCSBINDIR + "rcsdiff -r%(prev)s -r%(rev)s %(file)s </dev/null 2>&1"
SH_LOCK = RCSBINDIR + "rcs -l %(file)s </dev/null 2>&1"
SH_CHECKIN = RCSBINDIR + "ci -u %(file)s <%(tfn)s 2>&1"
# Titles for various output pages """
T_HOME = FAQNAME + " Wizard 0.2 (alpha)"
T_ERROR = "Sorry, an error occurred"
T_ROULETTE = FAQNAME + " Roulette"
T_ALL = "The Whole " + FAQNAME
T_INDEX = FAQNAME + " Index"
T_SEARCH = FAQNAME + " Search Results"
T_RECENT = "Recently Changed %s Entries" % FAQNAME
T_SHOW = FAQNAME + " Entry"
T_LOG = "RCS log for %s entry" % FAQNAME
T_DIFF = "RCS diff for %s entry" % FAQNAME
T_ADD = "How to add an entry to the " + FAQNAME
T_DELETE = "How to delete an entry from the " + FAQNAME
T_EDIT = FAQNAME + " Edit Wizard"
T_REVIEW = T_EDIT + " - Review Changes"
T_COMMITTED = T_EDIT + " - Changes Committed"
T_COMMITFAILED = T_EDIT + " - Commit Failed"
T_CANTCOMMIT = T_EDIT + " - Commit Rejected"
T_HELP = T_EDIT + " - Help"
# Titles of FAQ sections # Titles of FAQ sections
SECTION_TITLES = { SECTION_TITLES = {
1: "General information and availability", 1: "General information and availability",
2: "Python in the real world", 2: "Python in the real world",
@ -49,6 +18,77 @@ SECTION_TITLES = {
7: "Using Python on non-UNIX platforms", 7: "Using Python on non-UNIX platforms",
} }
# Parameters you definitely want to change
PASSWORD = "Spam" # Editing password
FAQNAME = "Python FAQ" # Name of the FAQ
OWNERNAME = "GvR" # Name for feedback
OWNEREMAIL = "guido@python.org" # Email for feedback
HOMEURL = "http://www.python.org" # Related home page
HOMENAME = "Python home" # Name of related home page
COOKIE_NAME = "Python-FAQ-Wizard" # Name used for Netscape cookie
RCSBINDIR = "/depot/gnu/plat/bin/" # Directory containing RCS commands
# (must end in a slash)
# Parameters you can normally leave alone
FAQCGI = 'faqw.py' # Relative URL of the FAQ cgi script
MAXHITS = 10 # Max #hits to be shown directly
COOKIE_LIFETIME = 28*24*3600 # Cookie expiration in seconds
# (28*24*3600 = 28 days = 4 weeks)
# Regular expression to recognize FAQ entry files: group(1) should be
# the section number, group(2) should be the question number. Both
# should be fixed width so simple-minded sorting yields the right
# order.
OKFILENAME = "^faq\([0-9][0-9]\)\.\([0-9][0-9][0-9]\)\.htp$"
# Format to construct a FAQ entry file name
NEWFILENAME = "faq%02d.%03d.htp"
# Version -- don't change unless you edit faqwiz.py
WIZVERSION = "0.3 (alpha)" # FAQ Wizard version
# ----------------------------------------------------------------------
# Anything below this point normally needn't be changed; you would
# change this if you were to create e.g. a French translation or if
# you just aren't happy with the text generated by the FAQ Wizard.
# Most strings here are subject to substitution (string%dictionary)
# RCS commands
SH_RLOG = RCSBINDIR + "rlog %(file)s </dev/null 2>&1"
SH_RLOG_H = RCSBINDIR + "rlog -h %(file)s </dev/null 2>&1"
SH_RDIFF = RCSBINDIR + "rcsdiff -r%(prev)s -r%(rev)s %(file)s </dev/null 2>&1"
SH_LOCK = RCSBINDIR + "rcs -l %(file)s </dev/null 2>&1"
SH_CHECKIN = RCSBINDIR + "ci -u %(file)s <%(tfn)s 2>&1"
# Titles for various output pages (not subject to substitution)
T_HOME = FAQNAME + " Wizard " + WIZVERSION
T_ERROR = "Sorry, an error occurred"
T_ROULETTE = FAQNAME + " Roulette"
T_ALL = "The Whole " + FAQNAME
T_INDEX = FAQNAME + " Index"
T_SEARCH = FAQNAME + " Search Results"
T_RECENT = "What's New in the " + FAQNAME
T_SHOW = FAQNAME + " Entry"
T_LOG = "RCS log for %s entry" % FAQNAME
T_DIFF = "RCS diff for %s entry" % FAQNAME
T_ADD = "Add an entry to the " + FAQNAME
T_DELETE = "Deleting an entry from the " + FAQNAME
T_EDIT = FAQNAME + " Edit Wizard"
T_REVIEW = T_EDIT + " - Review Changes"
T_COMMITTED = T_EDIT + " - Changes Committed"
T_COMMITFAILED = T_EDIT + " - Commit Failed"
T_CANTCOMMIT = T_EDIT + " - Commit Rejected"
T_HELP = T_EDIT + " - Help"
# Generic prologue and epilogue # Generic prologue and epilogue
PROLOGUE = ''' PROLOGUE = '''
@ -68,7 +108,7 @@ PROLOGUE = '''
EPILOGUE = ''' EPILOGUE = '''
<HR> <HR>
<A HREF="%(HOMEURL)s">%(HOMENAME)s</A> / <A HREF="%(HOMEURL)s">%(HOMENAME)s</A> /
<A HREF="%(FAQCGI)s?req=home">%(FAQNAME)s Wizard</A> / <A HREF="%(FAQCGI)s?req=home">%(FAQNAME)s Wizard %(WIZVERSION)s</A> /
Feedback to <A HREF="mailto:%(OWNEREMAIL)s">%(OWNERNAME)s</A> Feedback to <A HREF="mailto:%(OWNEREMAIL)s">%(OWNERNAME)s</A>
</BODY> </BODY>
@ -78,18 +118,41 @@ Feedback to <A HREF="mailto:%(OWNEREMAIL)s">%(OWNERNAME)s</A>
# Home page # Home page
HOME = """ HOME = """
<H2>Search the %(FAQNAME)s:</H2>
<BLOCKQUOTE>
<FORM ACTION="%(FAQCGI)s"> <FORM ACTION="%(FAQCGI)s">
<INPUT TYPE=text NAME=query> <INPUT TYPE=text NAME=query>
<INPUT TYPE=submit VALUE="Search"><BR> <INPUT TYPE=submit VALUE="Search"><BR>
(Case insensitive regular expressions.) <INPUT TYPE=radio NAME=querytype VALUE=simple CHECKED>
Simple string
/
<INPUT TYPE=radio NAME=querytype VALUE=regex>
Regular expression
<BR>
<INPUT TYPE=radio NAME=casefold VALUE=yes CHECKED>
Fold case
/
<INPUT TYPE=radio NAME=casefold VALUE=no>
Case sensitive
<BR>
<INPUT TYPE=hidden NAME=req VALUE=search> <INPUT TYPE=hidden NAME=req VALUE=search>
</FORM> </FORM>
</BLOCKQUOTE>
<HR>
<H2>Other forms of %(FAQNAME)s access:</H2>
<UL> <UL>
<LI><A HREF="%(FAQCGI)s?req=index">FAQ index</A> <LI><A HREF="%(FAQCGI)s?req=index">FAQ index</A>
<LI><A HREF="%(FAQCGI)s?req=all">The whole FAQ</A> <LI><A HREF="%(FAQCGI)s?req=all">The whole FAQ</A>
<LI><A HREF="%(FAQCGI)s?req=recent">Recently changed FAQ entries</A> <LI><A HREF="%(FAQCGI)s?req=recent">What's new in the FAQ?</A>
<LI><A HREF="%(FAQCGI)s?req=roulette">FAQ roulette</A> <LI><A HREF="%(FAQCGI)s?req=roulette">FAQ roulette</A>
<LI><A HREF="%(FAQCGI)s?req=add">Add a FAQ entry</A>
<LI><A HREF="%(FAQCGI)s?req=delete">Delete a FAQ entry</A>
</UL> </UL>
""" """
@ -98,23 +161,34 @@ HOME = """
INDEX_SECTION = """ INDEX_SECTION = """
<P> <P>
<HR> <HR>
<H2>%(sec)d. %(title)s</H2> <H2>%(sec)s. %(title)s</H2>
<UL> <UL>
""" """
INDEX_ADDSECTION = """
<P>
<LI><A HREF="%(FAQCGI)s?req=new&amp;section=%(sec)s">Add new entry</A>
(at this point)
"""
INDEX_ENDSECTION = """ INDEX_ENDSECTION = """
</UL> </UL>
""" """
INDEX_ENTRY = """\ INDEX_ENTRY = """\
<LI><A HREF="%(FAQCGI)s?req=show&file=%(file)s">%(title)s</A><BR> <LI><A HREF="%(FAQCGI)s?req=show&amp;file=%(file)s">%(title)s</A><BR>
""" """
# Entry formatting # Entry formatting
ENTRY_HEADER = """
<HR>
<H2>%(title)s</H2>
"""
ENTRY_FOOTER = """ ENTRY_FOOTER = """
<A HREF="%(FAQCGI)s?req=edit&file=%(file)s">Edit this entry</A> / <A HREF="%(FAQCGI)s?req=edit&amp;file=%(file)s">Edit this entry</A> /
<A HREF="%(FAQCGI)s?req=log&file=%(file)s">Log info</A> <A HREF="%(FAQCGI)s?req=log&amp;file=%(file)s">Log info</A>
""" """
ENTRY_LOGINFO = """ ENTRY_LOGINFO = """
@ -133,12 +207,12 @@ Your search matched the following entry:
""" """
FEW_HITS = """ FEW_HITS = """
Your search matched the following %(count)d entries: Your search matched the following %(count)s entries:
""" """
MANY_HITS = """ MANY_HITS = """
Your search matched more than %(MAXHITS)d entries. Your search matched more than %(MAXHITS)s entries.
The %(count)d matching entries are presented here ordered by section: The %(count)s matching entries are presented here ordered by section:
""" """
# RCS log and diff # RCS log and diff
@ -149,7 +223,7 @@ previous one.
""" """
DIFFLINK = """\ DIFFLINK = """\
<A HREF="%(FAQCGI)s?req=diff&file=%(file)s&rev=%(rev)s">%(line)s</A> <A HREF="%(FAQCGI)s?req=diff&amp;file=%(file)s&amp;rev=%(rev)s">%(line)s</A>
""" """
# Recently changed entries # Recently changed entries
@ -159,52 +233,34 @@ NO_RECENT = """
No %(FAQNAME)s entries were changed in the last %(period)s. No %(FAQNAME)s entries were changed in the last %(period)s.
""" """
ONE_RECENT = """ VIEW_MENU = """
<HR> <HR>
View entries changed in the last: View entries changed in the last...
<UL> <UL>
<LI><A HREF="%(FAQCGI)s?req=recent&days=1">24 hours</A> <LI><A HREF="%(FAQCGI)s?req=recent&amp;days=1">24 hours</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=2">2 days</A> <LI><A HREF="%(FAQCGI)s?req=recent&amp;days=2">2 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=3">3 days</A> <LI><A HREF="%(FAQCGI)s?req=recent&amp;days=3">3 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=7">week</A> <LI><A HREF="%(FAQCGI)s?req=recent&amp;days=7">week</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=28">4 weeks</A> <LI><A HREF="%(FAQCGI)s?req=recent&amp;days=28">4 weeks</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=365250">millennium</A> <LI><A HREF="%(FAQCGI)s?req=recent&amp;days=365250">millennium</A>
</UL> </UL>
"""
ONE_RECENT = VIEW_MENU + """
The following %(FAQNAME)s entry was changed in the last %(period)s: The following %(FAQNAME)s entry was changed in the last %(period)s:
""" """
SOME_RECENT = """ SOME_RECENT = VIEW_MENU + """
<HR> The following %(count)s %(FAQNAME)s entries were changed
View entries changed in the last:
<UL>
<LI><A HREF="%(FAQCGI)s?req=recent&days=1">24 hours</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=2">2 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=3">3 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=7">week</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=28">4 weeks</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=365250">millennium</A>
</UL>
The following %(count)d %(FAQNAME)s entries were changed
in the last %(period)s, most recently changed shown first: in the last %(period)s, most recently changed shown first:
""" """
TAIL_RECENT = """ TAIL_RECENT = VIEW_MENU
<HR>
View entries changed in the last:
<UL>
<LI><A HREF="%(FAQCGI)s?req=recent&days=1">24 hours</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=2">2 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=3">3 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=7">week</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=28">4 weeks</A>
<LI><A HREF="%(FAQCGI)s?req=recent&days=365250">millennium</A>
</UL>
"""
# Last changed banner on "all" (strftime format) # Last changed banner on "all" (strftime format)
LAST_CHANGED = "Last changed on %c %Z" LAST_CHANGED = "Last changed on %c %Z"
# "Compat" command prologue (no <BODY> tag) # "Compat" command prologue (this has no <BODY> tag)
COMPAT = """ COMPAT = """
<H1>The whole %(FAQNAME)s</H1> <H1>The whole %(FAQNAME)s</H1>
""" """
@ -261,8 +317,8 @@ Click this button to commit your changes.
""" """
NOCOMMIT = """ NOCOMMIT = """
You can't commit your changes unless you enter a log message, your To commit your changes, please enter a log message, your name, email
name, email addres, and the correct password in the form below. addres, and the correct password in the form below.
<HR> <HR>
""" """
@ -280,6 +336,27 @@ Please use your browser's Back command to correct the form and commit
again. again.
""" """
NEWCONFLICT = """
<P>
You are creating a new entry, but the entry number specified is not
correct.
<P>
The two most common causes of this problem are:
<UL>
<LI>After creating the entry yourself, you went back in your browser,
edited the entry some more, and clicked Commit again.
<LI>Someone else started creating a new entry in the same section and
committed before you did.
</UL>
(It is also possible that the last entry in the section was physically
deleted, but this should not happen except through manual intervention
by the FAQ maintainer.)
<P>
<A HREF="%(FAQCGI)s?req=new&amp;section=%(sec)s">Click here to try
again.</A>
<P>
"""
VERSIONCONFLICT = """ VERSIONCONFLICT = """
<P> <P>
You edited version %(editversion)s but the current version is %(version)s. You edited version %(editversion)s but the current version is %(version)s.
@ -292,8 +369,8 @@ The two most common causes of this problem are:
before you did. before you did.
</UL> </UL>
<P> <P>
<A HREF="%(FAQCGI)s?req=show&file=%(file)s">Click here to reload the entry <A HREF="%(FAQCGI)s?req=show&amp;file=%(file)s">Click here to reload
and try again.</A> the entry and try again.</A>
<P> <P>
""" """
@ -328,6 +405,37 @@ COMMITFAILED = """
Exit status %(sts)04x. Exit status %(sts)04x.
""" """
# Add/Delete
ADD_HEAD = """
At the moment, new entries can only be added at the end of a section.
This is because the entry numbers are also their
unique identifiers -- it's a bad idea to renumber entries.
<P>
Click on the section to which you want to add a new entry:
<UL>
"""
ADD_SECTION = """\
<LI><A HREF="%(FAQCGI)s?req=new&amp;section=%(section)s">%(section)s. %(title)s</A>
"""
ADD_TAIL = """
</UL>
"""
DELETE = """
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).
"""
# Help file for the FAQ Edit Wizard
HELP = """ HELP = """
Using the %(FAQNAME)s Edit Wizard speaks mostly for itself. Here are Using the %(FAQNAME)s Edit Wizard speaks mostly for itself. Here are
some answers to questions you are likely to ask: some answers to questions you are likely to ask:

View file

@ -1,6 +1,19 @@
import sys, string, time, os, stat, regex, cgi, faqconf """Generic FAQ Wizard.
from cgi import escape This is a CGI program that maintains a user-editable FAQ. It uses RCS
to keep track of changes to individual FAQ entries. It is fully
configurable; everything you might want to change when using this
program to maintain some other FAQ than the Python FAQ is contained in
the configuration module, faqconf.py.
Note that this is not an executable script; it's an importable module.
The actual script in cgi-bin minimal; it's appended at the end of this
file as a string literal.
"""
import sys, string, time, os, stat, regex, cgi, faqconf
from faqconf import * # This imports all uppercase names
class FileError: class FileError:
def __init__(self, file): def __init__(self, file):
@ -9,34 +22,60 @@ class FileError:
class InvalidFile(FileError): class InvalidFile(FileError):
pass pass
class NoSuchSection(FileError):
def __init__(self, section):
FileError.__init__(self, NEWFILENAME %(section, 1))
self.section = section
class NoSuchFile(FileError): class NoSuchFile(FileError):
def __init__(self, file, why=None): def __init__(self, file, why=None):
FileError.__init__(self, file) FileError.__init__(self, file)
self.why = why self.why = why
def replace(s, old, new):
try:
return string.replace(s, old, new)
except AttributeError:
return string.join(string.split(s, old), new)
def escape(s):
s = replace(s, '&', '&amp;')
s = replace(s, '<', '&lt;')
s = replace(s, '>', '&gt')
return s
def escapeq(s): def escapeq(s):
s = escape(s) s = escape(s)
import regsub s = replace(s, '"', '&quot;')
s = regsub.gsub('"', '&quot;', s)
return s return s
def interpolate(format, entry={}, kwdict={}, **kw): def _interpolate(format, args, kw):
s = format % MDict(kw, entry, kwdict, faqconf.__dict__) try:
return s quote = kw['_quote']
except KeyError:
quote = 1
d = (kw,) + args + (faqconf.__dict__,)
m = MagicDict(d, quote)
return format % m
def emit(format, entry={}, kwdict={}, file=sys.stdout, **kw): def interpolate(format, *args, **kw):
s = format % MDict(kw, entry, kwdict, faqconf.__dict__) return _interpolate(format, args, kw)
file.write(s)
def emit(format, *args, **kw):
try:
f = kw['_file']
except KeyError:
f = sys.stdout
f.write(_interpolate(format, args, kw))
translate_prog = None translate_prog = None
def translate(text): def translate(text):
global translate_prog global translate_prog
if not translate_prog: if not translate_prog:
import regex
url = '\(http\|ftp\)://[^ \t\r\n]*' url = '\(http\|ftp\)://[^ \t\r\n]*'
email = '\<[-a-zA-Z0-9._]+@[-a-zA-Z0-9._]+' email = '\<[-a-zA-Z0-9._]+@[-a-zA-Z0-9._]+'
translate_prog = prog = regex.compile(url + "\|" + email) translate_prog = prog = regex.compile(url + '\|' + email)
else: else:
prog = translate_prog prog = translate_prog
i = 0 i = 0
@ -45,10 +84,10 @@ def translate(text):
j = prog.search(text, i) j = prog.search(text, i)
if j < 0: if j < 0:
break break
list.append(cgi.escape(text[i:j])) list.append(escape(text[i:j]))
i = j i = j
url = prog.group(0) url = prog.group(0)
while url[-1] in ");:,.?'\"": while url[-1] in ');:,.?\'"':
url = url[:-1] url = url[:-1]
url = escape(url) url = escape(url)
if ':' in url: if ':' in url:
@ -58,7 +97,7 @@ def translate(text):
list.append(repl) list.append(repl)
i = i + len(url) i = i + len(url)
j = len(text) j = len(text)
list.append(cgi.escape(text[i:j])) list.append(escape(text[i:j]))
return string.join(list, '') return string.join(list, '')
emphasize_prog = None emphasize_prog = None
@ -67,12 +106,9 @@ def emphasize(line):
global emphasize_prog global emphasize_prog
import regsub import regsub
if not emphasize_prog: if not emphasize_prog:
import regex pat = '\*\([a-zA-Z]+\)\*'
pat = "\*\([a-zA-Z]+\)\*" emphasize_prog = regex.compile(pat)
emphasize_prog = prog = regex.compile(pat) return regsub.gsub(emphasize_prog, '<I>\\1</I>', line)
else:
prog = emphasize_prog
return regsub.gsub(prog, "<I>\\1</I>", line)
def load_cookies(): def load_cookies():
if not os.environ.has_key('HTTP_COOKIE'): if not os.environ.has_key('HTTP_COOKIE'):
@ -90,7 +126,7 @@ def load_cookies():
def load_my_cookie(): def load_my_cookie():
cookies = load_cookies() cookies = load_cookies()
try: try:
value = cookies[faqconf.COOKIE_NAME] value = cookies[COOKIE_NAME]
except KeyError: except KeyError:
return {} return {}
import urllib import urllib
@ -105,20 +141,35 @@ def load_my_cookie():
'email': email, 'email': email,
'password': password} 'password': password}
class MDict: def send_my_cookie(ui):
name = COOKIE_NAME
value = "%s/%s/%s" % (ui.author, ui.email, ui.password)
import urllib
value = urllib.quote(value)
now = time.time()
then = now + COOKIE_LIFETIME
gmt = time.gmtime(then)
print "Set-Cookie: %s=%s; path=/cgi-bin/;" % (name, value),
print time.strftime("expires=%a, %d-%b-%x %X GMT", gmt)
def __init__(self, *d): class MagicDict:
def __init__(self, d, quote):
self.__d = d self.__d = d
self.__quote = quote
def __getitem__(self, key): def __getitem__(self, key):
for d in self.__d: for d in self.__d:
try: try:
value = d[key] value = d[key]
if value: if value:
value = str(value)
if self.__quote:
value = escapeq(value)
return value return value
except KeyError: except KeyError:
pass pass
return "" return ''
class UserInput: class UserInput:
@ -140,17 +191,50 @@ class UserInput:
def __getitem__(self, key): def __getitem__(self, key):
return getattr(self, key) return getattr(self, key)
class FaqFormatter: class FaqEntry:
def __init__(self, entry): def __init__(self, fp, file, sec_num):
self.entry = entry self.file = file
self.sec, self.num = sec_num
if fp:
import rfc822
self.__headers = rfc822.Message(fp)
self.body = string.strip(fp.read())
else:
self.__headers = {'title': "%d.%d. " % sec_num}
self.body = ''
def __getattr__(self, name):
if name[0] == '_':
raise AttributeError
key = string.join(string.split(name, '_'), '-')
try:
value = self.__headers[key]
except KeyError:
value = ''
setattr(self, name, value)
return value
def __getitem__(self, key):
return getattr(self, key)
def load_version(self):
command = interpolate(SH_RLOG_H, self)
p = os.popen(command)
version = ''
while 1:
line = p.readline()
if not line:
break
if line[:5] == 'head:':
version = string.strip(line[5:])
p.close()
self.version = version
def show(self, edit=1): def show(self, edit=1):
entry = self.entry emit(ENTRY_HEADER, self)
print "<HR>"
print "<H2>%s</H2>" % escape(entry.title)
pre = 0 pre = 0
for line in string.split(entry.body, '\n'): for line in string.split(self.body, '\n'):
if not string.strip(line): if not string.strip(line):
if pre: if pre:
print '</PRE>' print '</PRE>'
@ -178,57 +262,16 @@ class FaqFormatter:
pre = 0 pre = 0
if edit: if edit:
print '<P>' print '<P>'
emit(faqconf.ENTRY_FOOTER, self.entry) emit(ENTRY_FOOTER, self)
if self.entry.last_changed_date: if self.last_changed_date:
emit(faqconf.ENTRY_LOGINFO, self.entry) emit(ENTRY_LOGINFO, self)
print '<P>' print '<P>'
class FaqEntry:
formatterclass = FaqFormatter
def __init__(self, fp, file, sec_num):
import rfc822
self.file = file
self.sec, self.num = sec_num
self.__headers = rfc822.Message(fp)
self.body = string.strip(fp.read())
def __getattr__(self, name):
if name[0] == '_':
raise AttributeError
key = string.join(string.split(name, '_'), '-')
try:
value = self.__headers[key]
except KeyError:
value = ''
setattr(self, name, value)
return value
def __getitem__(self, key):
return getattr(self, key)
def show(self, edit=1):
self.formatterclass(self).show(edit=edit)
def load_version(self):
command = interpolate(faqconf.SH_RLOG_H, self)
p = os.popen(command)
version = ""
while 1:
line = p.readline()
if not line:
break
if line[:5] == 'head:':
version = string.strip(line[5:])
p.close()
self.version = version
class FaqDir: class FaqDir:
entryclass = FaqEntry entryclass = FaqEntry
__okprog = regex.compile('^faq\([0-9][0-9]\)\.\([0-9][0-9][0-9]\)\.htp$') __okprog = regex.compile(OKFILENAME)
def __init__(self, dir=os.curdir): def __init__(self, dir=os.curdir):
self.__dir = dir self.__dir = dir
@ -279,8 +322,17 @@ class FaqDir:
def show(self, file, edit=1): def show(self, file, edit=1):
self.open(file).show(edit=edit) self.open(file).show(edit=edit)
def new(self, sec): def new(self, section):
XXX if not SECTION_TITLES.has_key(section):
raise NoSuchSection(section)
maxnum = 0
for file in self.list():
sec, num = self.parse(file)
if sec == section:
maxnum = max(maxnum, num)
sec_num = (section, maxnum+1)
file = NEWFILENAME % sec_num
return self.entryclass(None, file, sec_num)
class FaqWizard: class FaqWizard:
@ -289,13 +341,13 @@ class FaqWizard:
self.dir = FaqDir() self.dir = FaqDir()
def go(self): def go(self):
print "Content-type: text/html" print 'Content-type: text/html'
req = self.ui.req or "home" req = self.ui.req or 'home'
mname = 'do_%s' % req mname = 'do_%s' % req
try: try:
meth = getattr(self, mname) meth = getattr(self, mname)
except AttributeError: except AttributeError:
self.error("Bad request %s" % `req`) self.error("Bad request type %s." % `req`)
else: else:
try: try:
meth() meth()
@ -303,29 +355,43 @@ class FaqWizard:
self.error("Invalid entry file name %s" % exc.file) self.error("Invalid entry file name %s" % exc.file)
except NoSuchFile, exc: except NoSuchFile, exc:
self.error("No entry with file name %s" % exc.file) self.error("No entry with file name %s" % exc.file)
except NoSuchSection, exc:
self.error("No section number %s" % exc.section)
self.epilogue() self.epilogue()
def error(self, message, **kw): def error(self, message, **kw):
self.prologue(faqconf.T_ERROR) self.prologue(T_ERROR)
apply(emit, (message,), kw) emit(message, kw)
def prologue(self, title, entry=None, **kw): def prologue(self, title, entry=None, **kw):
emit(faqconf.PROLOGUE, entry, kwdict=kw, title=escape(title)) emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
def epilogue(self): def epilogue(self):
emit(faqconf.EPILOGUE) emit(EPILOGUE)
def do_home(self): def do_home(self):
self.prologue(faqconf.T_HOME) self.prologue(T_HOME)
emit(faqconf.HOME) emit(HOME)
def do_debug(self):
self.prologue("FAQ Wizard Debugging")
form = cgi.FieldStorage()
cgi.print_form(form)
cgi.print_environ(os.environ)
cgi.print_directory()
cgi.print_arguments()
def do_search(self): def do_search(self):
query = self.ui.query query = self.ui.query
if not query: if not query:
self.error("No query string") self.error("Empty query string!")
return return
self.prologue(faqconf.T_SEARCH) self.prologue(T_SEARCH)
if self.ui.casefold == "no": if self.ui.querytype != 'regex':
for c in '\\.[]?+^$*':
if c in query:
query = replace(query, c, '\\'+c)
if self.ui.casefold == 'no':
p = regex.compile(query) p = regex.compile(query)
else: else:
p = regex.compile(query, regex.casefold) p = regex.compile(query, regex.casefold)
@ -338,26 +404,26 @@ class FaqWizard:
if p.search(entry.title) >= 0 or p.search(entry.body) >= 0: if p.search(entry.title) >= 0 or p.search(entry.body) >= 0:
hits.append(file) hits.append(file)
if not hits: if not hits:
emit(faqconf.NO_HITS, count=0) emit(NO_HITS, self.ui, count=0)
elif len(hits) <= faqconf.MAXHITS: elif len(hits) <= MAXHITS:
if len(hits) == 1: if len(hits) == 1:
emit(faqconf.ONE_HIT, count=1) emit(ONE_HIT, count=1)
else: else:
emit(faqconf.FEW_HITS, count=len(hits)) emit(FEW_HITS, count=len(hits))
self.format_all(hits) self.format_all(hits)
else: else:
emit(faqconf.MANY_HITS, count=len(hits)) emit(MANY_HITS, count=len(hits))
self.format_index(hits) self.format_index(hits)
def do_all(self): def do_all(self):
self.prologue(faqconf.T_ALL) self.prologue(T_ALL)
files = self.dir.list() files = self.dir.list()
self.last_changed(files) self.last_changed(files)
self.format_all(files) self.format_all(files)
def do_compat(self): def do_compat(self):
files = self.dir.list() files = self.dir.list()
emit(faqconf.COMPAT) emit(COMPAT)
self.last_changed(files) self.last_changed(files)
self.format_all(files, edit=0) self.format_all(files, edit=0)
sys.exit(0) sys.exit(0)
@ -372,7 +438,7 @@ class FaqWizard:
mtime = st[stat.ST_MTIME] mtime = st[stat.ST_MTIME]
if mtime > latest: if mtime > latest:
latest = mtime latest = mtime
print time.strftime(faqconf.LAST_CHANGED, print time.strftime(LAST_CHANGED,
time.localtime(time.time())) time.localtime(time.time()))
def format_all(self, files, edit=1): def format_all(self, files, edit=1):
@ -380,10 +446,10 @@ class FaqWizard:
self.dir.show(file, edit=edit) self.dir.show(file, edit=edit)
def do_index(self): def do_index(self):
self.prologue(faqconf.T_INDEX) self.prologue(T_INDEX)
self.format_index(self.dir.list()) self.format_index(self.dir.list(), add=1)
def format_index(self, files): def format_index(self, files, add=0):
sec = 0 sec = 0
for file in files: for file in files:
try: try:
@ -392,14 +458,16 @@ class FaqWizard:
continue continue
if entry.sec != sec: if entry.sec != sec:
if sec: if sec:
emit(faqconf.INDEX_ENDSECTION, sec=sec) if add:
emit(INDEX_ADDSECTION, sec=sec)
emit(INDEX_ENDSECTION, sec=sec)
sec = entry.sec sec = entry.sec
emit(faqconf.INDEX_SECTION, emit(INDEX_SECTION, sec=sec, title=SECTION_TITLES[sec])
sec=sec, emit(INDEX_ENTRY, entry)
title=faqconf.SECTION_TITLES[sec])
emit(faqconf.INDEX_ENTRY, entry)
if sec: if sec:
emit(faqconf.INDEX_ENDSECTION, sec=sec) if add:
emit(INDEX_ADDSECTION, sec=sec)
emit(INDEX_ENDSECTION, sec=sec)
def do_recent(self): def do_recent(self):
if not self.ui.days: if not self.ui.days:
@ -422,53 +490,58 @@ class FaqWizard:
list.append((mtime, file)) list.append((mtime, file))
list.sort() list.sort()
list.reverse() list.reverse()
self.prologue(faqconf.T_RECENT) self.prologue(T_RECENT)
if days <= 1: if days <= 1:
period = "%.2g hours" % (days*24) period = "%.2g hours" % (days*24)
else: else:
period = "%.6g days" % days period = "%.6g days" % days
if not list: if not list:
emit(faqconf.NO_RECENT, period=period) emit(NO_RECENT, period=period)
elif len(list) == 1: elif len(list) == 1:
emit(faqconf.ONE_RECENT, period=period) emit(ONE_RECENT, period=period)
else: else:
emit(faqconf.SOME_RECENT, period=period, count=len(list)) emit(SOME_RECENT, period=period, count=len(list))
self.format_all(map(lambda (mtime, file): file, list)) self.format_all(map(lambda (mtime, file): file, list))
emit(faqconf.TAIL_RECENT) emit(TAIL_RECENT)
def do_roulette(self): def do_roulette(self):
self.prologue(faqconf.T_ROULETTE) self.prologue(T_ROULETTE)
file = self.dir.roulette() file = self.dir.roulette()
self.dir.show(file) self.dir.show(file)
def do_help(self): def do_help(self):
self.prologue(faqconf.T_HELP) self.prologue(T_HELP)
emit(faqconf.HELP) emit(HELP)
def do_show(self): def do_show(self):
entry = self.dir.open(self.ui.file) entry = self.dir.open(self.ui.file)
self.prologue("Python FAQ Entry") self.prologue(T_SHOW)
entry.show() entry.show()
def do_add(self): def do_add(self):
self.prologue(T_ADD) self.prologue(T_ADD)
self.error("Not yet implemented") emit(ADD_HEAD)
sections = SECTION_TITLES.items()
sections.sort()
for section, title in sections:
emit(ADD_SECTION, section=section, title=title)
emit(ADD_TAIL)
def do_delete(self): def do_delete(self):
self.prologue(T_DELETE) self.prologue(T_DELETE)
self.error("Not yet implemented") emit(DELETE)
def do_log(self): def do_log(self):
entry = self.dir.open(self.ui.file) entry = self.dir.open(self.ui.file)
self.prologue(faqconf.T_LOG, entry) self.prologue(T_LOG, entry)
emit(faqconf.LOG, entry) emit(LOG, entry)
self.rlog(interpolate(faqconf.SH_RLOG, entry), entry) self.rlog(interpolate(SH_RLOG, entry), entry)
def rlog(self, command, entry=None): def rlog(self, command, entry=None):
output = os.popen(command).read() output = os.popen(command).read()
sys.stdout.write("<PRE>") sys.stdout.write('<PRE>')
athead = 0 athead = 0
lines = string.split(output, "\n") lines = string.split(output, '\n')
while lines and not lines[-1]: while lines and not lines[-1]:
del lines[-1] del lines[-1]
if lines: if lines:
@ -479,8 +552,8 @@ class FaqWizard:
for line in lines: for line in lines:
if entry and athead and line[:9] == 'revision ': if entry and athead and line[:9] == 'revision ':
rev = string.strip(line[9:]) rev = string.strip(line[9:])
if rev != "1.1": if rev != '1.1':
emit(faqconf.DIFFLINK, entry, rev=rev, line=line) emit(DIFFLINK, entry, rev=rev, line=line)
else: else:
print line print line
athead = 0 athead = 0
@ -489,61 +562,76 @@ class FaqWizard:
if line[:1] == '-' and len(line) >= 20 and \ if line[:1] == '-' and len(line) >= 20 and \
line == len(line) * line[0]: line == len(line) * line[0]:
athead = 1 athead = 1
sys.stdout.write("<HR>") sys.stdout.write('<HR>')
else: else:
print line print line
print "</PRE>" print '</PRE>'
def do_diff(self): def do_diff(self):
entry = self.dir.open(self.ui.file) entry = self.dir.open(self.ui.file)
rev = self.ui.rev rev = self.ui.rev
r = regex.compile( r = regex.compile(
"^\([1-9][0-9]?[0-9]?\)\.\([1-9][0-9]?[0-9]?[0-9]?\)$") '^\([1-9][0-9]?[0-9]?\)\.\([1-9][0-9]?[0-9]?[0-9]?\)$')
if r.match(rev) < 0: if r.match(rev) < 0:
self.error("Invalid revision number: %s" % `rev`) self.error("Invalid revision number: %s." % `rev`)
[major, minor] = map(string.atoi, r.group(1, 2)) [major, minor] = map(string.atoi, r.group(1, 2))
if minor == 1: if minor == 1:
self.error("No previous revision") self.error("No previous revision.")
return return
prev = "%d.%d" % (major, minor-1) prev = '%d.%d' % (major, minor-1)
self.prologue(faqconf.T_DIFF, entry) self.prologue(T_DIFF, entry)
self.shell(interpolate(faqconf.SH_RDIFF, entry, rev=rev, prev=prev)) self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
def shell(self, command): def shell(self, command):
output = os.popen(command).read() output = os.popen(command).read()
sys.stdout.write("<PRE>") sys.stdout.write('<PRE>')
print escape(output) print escape(output)
print "</PRE>" print '</PRE>'
def do_new(self): def do_new(self):
editor = FaqEditor(self.ui, self.dir.new(self.file)) entry = self.dir.new(section=string.atoi(self.ui.section))
self.prologue(faqconf.T_NEW) entry.version = '*new*'
self.error("Not yet implemented") self.prologue(T_EDIT)
emit(EDITHEAD)
emit(EDITFORM1, entry, editversion=entry.version)
emit(EDITFORM2, entry, load_my_cookie())
emit(EDITFORM3)
entry.show(edit=0)
def do_edit(self): def do_edit(self):
entry = self.dir.open(self.ui.file) entry = self.dir.open(self.ui.file)
entry.load_version() entry.load_version()
self.prologue(faqconf.T_EDIT) self.prologue(T_EDIT)
emit(faqconf.EDITHEAD) emit(EDITHEAD)
emit(faqconf.EDITFORM1, entry, editversion=entry.version) emit(EDITFORM1, entry, editversion=entry.version)
emit(faqconf.EDITFORM2, entry, load_my_cookie(), log=self.ui.log) emit(EDITFORM2, entry, load_my_cookie())
emit(faqconf.EDITFORM3) emit(EDITFORM3)
entry.show(edit=0) entry.show(edit=0)
def do_review(self): def do_review(self):
send_my_cookie(self.ui)
if self.ui.editversion == '*new*':
sec, num = self.dir.parse(self.ui.file)
entry = self.dir.new(section=sec)
entry.version = "*new*"
if entry.file != self.ui.file:
self.error("Commit version conflict!")
emit(NEWCONFLICT, self.ui, sec=sec, num=num)
return
else:
entry = self.dir.open(self.ui.file) entry = self.dir.open(self.ui.file)
entry.load_version() entry.load_version()
# Check that the FAQ entry number didn't change # Check that the FAQ entry number didn't change
if string.split(self.ui.title)[:1] != string.split(entry.title)[:1]: if string.split(self.ui.title)[:1] != string.split(entry.title)[:1]:
self.error("Don't change the FAQ entry number please.") self.error("Don't change the entry number please!")
return return
# Check that the edited version is the current version # Check that the edited version is the current version
if entry.version != self.ui.editversion: if entry.version != self.ui.editversion:
self.error("Version conflict.") self.error("Commit version conflict!")
emit(faqconf.VERSIONCONFLICT, entry, self.ui) emit(VERSIONCONFLICT, entry, self.ui)
return return
commit_ok = ((not faqconf.PASSWORD commit_ok = ((not PASSWORD
or self.ui.password == faqconf.PASSWORD) or self.ui.password == PASSWORD)
and self.ui.author and self.ui.author
and '@' in self.ui.email and '@' in self.ui.email
and self.ui.log) and self.ui.log)
@ -551,40 +639,45 @@ class FaqWizard:
if not commit_ok: if not commit_ok:
self.cantcommit() self.cantcommit()
else: else:
self.commit() self.commit(entry)
return return
self.prologue(faqconf.T_REVIEW) self.prologue(T_REVIEW)
emit(faqconf.REVIEWHEAD) emit(REVIEWHEAD)
entry.body = self.ui.body entry.body = self.ui.body
entry.title = self.ui.title entry.title = self.ui.title
entry.show(edit=0) entry.show(edit=0)
emit(faqconf.EDITFORM1, entry, self.ui) emit(EDITFORM1, self.ui, entry)
if commit_ok: if commit_ok:
emit(faqconf.COMMIT) emit(COMMIT)
else: else:
emit(faqconf.NOCOMMIT) emit(NOCOMMIT)
emit(faqconf.EDITFORM2, entry, load_my_cookie(), log=self.ui.log) emit(EDITFORM2, self.ui, entry, load_my_cookie())
emit(faqconf.EDITFORM3) emit(EDITFORM3)
def cantcommit(self): def cantcommit(self):
self.prologue(faqconf.T_CANTCOMMIT) self.prologue(T_CANTCOMMIT)
print faqconf.CANTCOMMIT_HEAD print CANTCOMMIT_HEAD
if not self.ui.passwd: if not self.ui.passwd:
emit(faqconf.NEED_PASSWD) emit(NEED_PASSWD)
if not self.ui.log: if not self.ui.log:
emit(faqconf.NEED_LOG) emit(NEED_LOG)
if not self.ui.author: if not self.ui.author:
emit(faqconf.NEED_AUTHOR) emit(NEED_AUTHOR)
if not self.ui.email: if not self.ui.email:
emit(faqconf.NEED_EMAIL) emit(NEED_EMAIL)
print faqconf.CANTCOMMIT_TAIL print CANTCOMMIT_TAIL
def commit(self): def commit(self, entry):
file = self.ui.file file = entry.file
entry = self.dir.open(file) # Normalize line endings in body
# Chech that there were any changes if '\r' in self.ui.body:
import regsub
self.ui.body = regsub.gsub('\r\n?', '\n', self.ui.body)
# Normalize whitespace in title
self.ui.title = string.join(string.split(self.ui.title))
# Check that there were any changes
if self.ui.body == entry.body and self.ui.title == entry.title: if self.ui.body == entry.body and self.ui.title == entry.title:
self.error("No changes.") self.error("You didn't make any changes!")
return return
# XXX Should lock here # XXX Should lock here
try: try:
@ -592,25 +685,25 @@ class FaqWizard:
except os.error: except os.error:
pass pass
try: try:
f = open(file, "w") f = open(file, 'w')
except IOError, why: except IOError, why:
self.error(faqconf.CANTWRITE, file=file, why=why) self.error(CANTWRITE, file=file, why=why)
return return
date = time.ctime(time.time()) date = time.ctime(time.time())
emit(faqconf.FILEHEADER, self.ui, os.environ, date=date, file=f) emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0)
f.write("\n") f.write('\n')
f.write(self.ui.body) f.write(self.ui.body)
f.write("\n") f.write('\n')
f.close() f.close()
import tempfile import tempfile
tfn = tempfile.mktemp() tfn = tempfile.mktemp()
f = open(tfn, "w") f = open(tfn, 'w')
emit(faqconf.LOGHEADER, self.ui, os.environ, date=date, file=f) emit(LOGHEADER, self.ui, os.environ, date=date, _file=f)
f.close() f.close()
command = interpolate( command = interpolate(
faqconf.SH_LOCK + "\n" + faqconf.SH_CHECKIN, SH_LOCK + '\n' + SH_CHECKIN,
file=file, tfn=tfn) file=file, tfn=tfn)
p = os.popen(command) p = os.popen(command)
@ -618,12 +711,12 @@ class FaqWizard:
sts = p.close() sts = p.close()
# XXX Should unlock here # XXX Should unlock here
if not sts: if not sts:
self.prologue(faqconf.T_COMMITTED) self.prologue(T_COMMITTED)
emit(faqconf.COMMITTED) emit(COMMITTED)
else: else:
self.error(faqconf.T_COMMITFAILED) self.error(T_COMMITFAILED)
emit(faqconf.COMMITFAILED, sts=sts) emit(COMMITFAILED, sts=sts)
print "<PRE>%s</PRE>" % cgi.escape(output) print '<PRE>%s</PRE>' % escape(output)
try: try:
os.unlink(tfn) os.unlink(tfn)
@ -636,31 +729,17 @@ class FaqWizard:
wiz = FaqWizard() wiz = FaqWizard()
wiz.go() wiz.go()
# This bootstrap script should be placed in your cgi-bin directory.
# You only need to edit the first two lines: change
# /usr/local/bin/python to where your Python interpreter lives change
# the value for FAQDIR to where your FAQ lives. The faqwiz.py and
# faqconf.py files should live there, too.
BOOTSTRAP = """\ BOOTSTRAP = """\
#! /usr/local/bin/python #! /usr/local/bin/python
FAQDIR = "/usr/people/guido/python/FAQ" FAQDIR = "/usr/people/guido/python/FAQ"
import sys, os
# This bootstrap script should be placed in your cgi-bin directory.
# You only need to edit the first two lines (above): Change
# /usr/local/bin/python to where your Python interpreter lives (you
# can't use /usr/bin/env here!); change FAQDIR to where your FAQ
# lives. The faqwiz.py and faqconf.py files should live there, too.
import posix
t1 = posix.times()
import os, sys, time, operator
os.chdir(FAQDIR) os.chdir(FAQDIR)
sys.path.insert(0, FAQDIR) sys.path.insert(0, FAQDIR)
try:
import faqwiz import faqwiz
except SystemExit, n:
sys.exit(n)
except:
t, v, tb = sys.exc_type, sys.exc_value, sys.exc_traceback
print
import cgi
cgi.print_exception(t, v, tb)
t2 = posix.times()
fmt = "<BR>(times: user %.3g, sys %.3g, ch-user %.3g, ch-sys %.3g, real %.3g)"
print fmt % tuple(map(operator.sub, t2, t1))
""" """