diff options
author | Guido van Rossum <guido@python.org> | 1997-05-26 05:43:29 (GMT) |
---|---|---|
committer | Guido van Rossum <guido@python.org> | 1997-05-26 05:43:29 (GMT) |
commit | ea31ea2859a005f8a794f715df925a1385bdc435 (patch) | |
tree | 16ce6cc781efa4e9e0d1ae9b42ce9463b6b3a928 | |
parent | 1677e5b5ddea42814f3c933c22da0779fc538f81 (diff) | |
download | cpython-ea31ea2859a005f8a794f715df925a1385bdc435.zip cpython-ea31ea2859a005f8a794f715df925a1385bdc435.tar.gz cpython-ea31ea2859a005f8a794f715df925a1385bdc435.tar.bz2 |
I'm happy with this.
-rw-r--r-- | Tools/faqwiz/faqconf.py | 248 | ||||
-rw-r--r-- | Tools/faqwiz/faqwiz.py | 493 |
2 files changed, 464 insertions, 277 deletions
diff --git a/Tools/faqwiz/faqconf.py b/Tools/faqwiz/faqconf.py index de82e22..2cd4b9e 100644 --- a/Tools/faqwiz/faqconf.py +++ b/Tools/faqwiz/faqconf.py @@ -1,36 +1,87 @@ -# Miscellaneous customization constants -PASSWORD = "Spam" # Edit password. Change this! -FAQCGI = 'faqw.py' # Relative URL of the FAQ cgi script +"""FAQ Wizard customization module. + +Edit this file to customize the FAQ Wizard. For normal purposes, you +should only have to change the FAQ section titles and the small group +of parameters below it. + +""" + +# Titles of FAQ sections + +SECTION_TITLES = { + 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", +} + +# 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 -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 +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 -RCSBINDIR = "/depot/gnu/plat/bin/" # Directory containing 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 -T_HOME = FAQNAME + " Wizard 0.2 (alpha)" +# 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 = "Recently Changed %s Entries" % FAQNAME +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 = "How to add an entry to the " + FAQNAME -T_DELETE = "How to delete an entry from the " + 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" @@ -38,17 +89,6 @@ T_COMMITFAILED = T_EDIT + " - Commit Failed" T_CANTCOMMIT = T_EDIT + " - Commit Rejected" T_HELP = T_EDIT + " - Help" -# Titles of FAQ sections -SECTION_TITLES = { - 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", -} - # Generic prologue and epilogue PROLOGUE = ''' @@ -68,7 +108,7 @@ PROLOGUE = ''' EPILOGUE = ''' <HR> <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> </BODY> @@ -78,18 +118,41 @@ Feedback to <A HREF="mailto:%(OWNEREMAIL)s">%(OWNERNAME)s</A> # Home page HOME = """ +<H2>Search the %(FAQNAME)s:</H2> + +<BLOCKQUOTE> + <FORM ACTION="%(FAQCGI)s"> <INPUT TYPE=text NAME=query> <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> </FORM> +</BLOCKQUOTE> + +<HR> + +<H2>Other forms of %(FAQNAME)s access:</H2> + <UL> <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=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=add">Add a FAQ entry</A> +<LI><A HREF="%(FAQCGI)s?req=delete">Delete a FAQ entry</A> </UL> """ @@ -98,23 +161,34 @@ HOME = """ INDEX_SECTION = """ <P> <HR> -<H2>%(sec)d. %(title)s</H2> +<H2>%(sec)s. %(title)s</H2> <UL> """ +INDEX_ADDSECTION = """ +<P> +<LI><A HREF="%(FAQCGI)s?req=new&section=%(sec)s">Add new entry</A> +(at this point) +""" + INDEX_ENDSECTION = """ </UL> """ INDEX_ENTRY = """\ -<LI><A HREF="%(FAQCGI)s?req=show&file=%(file)s">%(title)s</A><BR> +<LI><A HREF="%(FAQCGI)s?req=show&file=%(file)s">%(title)s</A><BR> """ # Entry formatting +ENTRY_HEADER = """ +<HR> +<H2>%(title)s</H2> +""" + ENTRY_FOOTER = """ -<A HREF="%(FAQCGI)s?req=edit&file=%(file)s">Edit this entry</A> / -<A HREF="%(FAQCGI)s?req=log&file=%(file)s">Log info</A> +<A HREF="%(FAQCGI)s?req=edit&file=%(file)s">Edit this entry</A> / +<A HREF="%(FAQCGI)s?req=log&file=%(file)s">Log info</A> """ ENTRY_LOGINFO = """ @@ -133,12 +207,12 @@ Your search matched the following entry: """ FEW_HITS = """ -Your search matched the following %(count)d entries: +Your search matched the following %(count)s entries: """ MANY_HITS = """ -Your search matched more than %(MAXHITS)d entries. -The %(count)d matching entries are presented here ordered by section: +Your search matched more than %(MAXHITS)s entries. +The %(count)s matching entries are presented here ordered by section: """ # RCS log and diff @@ -149,7 +223,7 @@ previous one. """ DIFFLINK = """\ -<A HREF="%(FAQCGI)s?req=diff&file=%(file)s&rev=%(rev)s">%(line)s</A> +<A HREF="%(FAQCGI)s?req=diff&file=%(file)s&rev=%(rev)s">%(line)s</A> """ # Recently changed entries @@ -159,52 +233,34 @@ NO_RECENT = """ No %(FAQNAME)s entries were changed in the last %(period)s. """ -ONE_RECENT = """ +VIEW_MENU = """ <HR> -View entries changed in the last: +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> +<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> +""" + +ONE_RECENT = VIEW_MENU + """ The following %(FAQNAME)s entry was changed in the last %(period)s: """ -SOME_RECENT = """ -<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> -The following %(count)d %(FAQNAME)s entries were changed +SOME_RECENT = VIEW_MENU + """ +The following %(count)s %(FAQNAME)s entries were changed in the last %(period)s, most recently changed shown first: """ -TAIL_RECENT = """ -<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> -""" +TAIL_RECENT = VIEW_MENU # Last changed banner on "all" (strftime format) LAST_CHANGED = "Last changed on %c %Z" -# "Compat" command prologue (no <BODY> tag) +# "Compat" command prologue (this has no <BODY> tag) COMPAT = """ <H1>The whole %(FAQNAME)s</H1> """ @@ -261,8 +317,8 @@ Click this button to commit your changes. """ NOCOMMIT = """ -You can't commit your changes unless you enter a log message, your -name, email addres, and the correct password in the form below. +To commit your changes, please enter a log message, your name, email +addres, and the correct password in the form below. <HR> """ @@ -280,6 +336,27 @@ Please use your browser's Back command to correct the form and commit 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&section=%(sec)s">Click here to try +again.</A> +<P> +""" + VERSIONCONFLICT = """ <P> 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. </UL> <P> -<A HREF="%(FAQCGI)s?req=show&file=%(file)s">Click here to reload the entry -and try again.</A> +<A HREF="%(FAQCGI)s?req=show&file=%(file)s">Click here to reload +the entry and try again.</A> <P> """ @@ -328,6 +405,37 @@ COMMITFAILED = """ 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&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 = """ Using the %(FAQNAME)s Edit Wizard speaks mostly for itself. Here are some answers to questions you are likely to ask: diff --git a/Tools/faqwiz/faqwiz.py b/Tools/faqwiz/faqwiz.py index 47aa3b7..a901a28 100644 --- a/Tools/faqwiz/faqwiz.py +++ b/Tools/faqwiz/faqwiz.py @@ -1,6 +1,19 @@ -import sys, string, time, os, stat, regex, cgi, faqconf +"""Generic FAQ Wizard. + +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. -from cgi import escape +""" + +import sys, string, time, os, stat, regex, cgi, faqconf +from faqconf import * # This imports all uppercase names class FileError: def __init__(self, file): @@ -9,34 +22,60 @@ class FileError: class InvalidFile(FileError): pass +class NoSuchSection(FileError): + def __init__(self, section): + FileError.__init__(self, NEWFILENAME %(section, 1)) + self.section = section + class NoSuchFile(FileError): def __init__(self, file, why=None): FileError.__init__(self, file) 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, '&', '&') + s = replace(s, '<', '<') + s = replace(s, '>', '>') + return s + def escapeq(s): s = escape(s) - import regsub - s = regsub.gsub('"', '"', s) + s = replace(s, '"', '"') return s -def interpolate(format, entry={}, kwdict={}, **kw): - s = format % MDict(kw, entry, kwdict, faqconf.__dict__) - return s +def _interpolate(format, args, kw): + try: + quote = kw['_quote'] + except KeyError: + quote = 1 + d = (kw,) + args + (faqconf.__dict__,) + m = MagicDict(d, quote) + return format % m + +def interpolate(format, *args, **kw): + return _interpolate(format, args, kw) -def emit(format, entry={}, kwdict={}, file=sys.stdout, **kw): - s = format % MDict(kw, entry, kwdict, faqconf.__dict__) - 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 def translate(text): global translate_prog if not translate_prog: - import regex url = '\(http\|ftp\)://[^ \t\r\n]*' email = '\<[-a-zA-Z0-9._]+@[-a-zA-Z0-9._]+' - translate_prog = prog = regex.compile(url + "\|" + email) + translate_prog = prog = regex.compile(url + '\|' + email) else: prog = translate_prog i = 0 @@ -45,10 +84,10 @@ def translate(text): j = prog.search(text, i) if j < 0: break - list.append(cgi.escape(text[i:j])) + list.append(escape(text[i:j])) i = j url = prog.group(0) - while url[-1] in ");:,.?'\"": + while url[-1] in ');:,.?\'"': url = url[:-1] url = escape(url) if ':' in url: @@ -58,7 +97,7 @@ def translate(text): list.append(repl) i = i + len(url) j = len(text) - list.append(cgi.escape(text[i:j])) + list.append(escape(text[i:j])) return string.join(list, '') emphasize_prog = None @@ -67,12 +106,9 @@ def emphasize(line): global emphasize_prog import regsub if not emphasize_prog: - import regex - pat = "\*\([a-zA-Z]+\)\*" - emphasize_prog = prog = regex.compile(pat) - else: - prog = emphasize_prog - return regsub.gsub(prog, "<I>\\1</I>", line) + pat = '\*\([a-zA-Z]+\)\*' + emphasize_prog = regex.compile(pat) + return regsub.gsub(emphasize_prog, '<I>\\1</I>', line) def load_cookies(): if not os.environ.has_key('HTTP_COOKIE'): @@ -90,7 +126,7 @@ def load_cookies(): def load_my_cookie(): cookies = load_cookies() try: - value = cookies[faqconf.COOKIE_NAME] + value = cookies[COOKIE_NAME] except KeyError: return {} import urllib @@ -105,20 +141,35 @@ def load_my_cookie(): 'email': email, '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) + +class MagicDict: - def __init__(self, *d): + def __init__(self, d, quote): self.__d = d + self.__quote = quote def __getitem__(self, key): for d in self.__d: try: value = d[key] if value: + value = str(value) + if self.__quote: + value = escapeq(value) return value except KeyError: pass - return "" + return '' class UserInput: @@ -140,17 +191,50 @@ class UserInput: def __getitem__(self, key): return getattr(self, key) -class FaqFormatter: +class FaqEntry: + + def __init__(self, fp, file, sec_num): + 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 __init__(self, entry): - self.entry = entry + 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): - entry = self.entry - print "<HR>" - print "<H2>%s</H2>" % escape(entry.title) + emit(ENTRY_HEADER, self) pre = 0 - for line in string.split(entry.body, '\n'): + for line in string.split(self.body, '\n'): if not string.strip(line): if pre: print '</PRE>' @@ -178,57 +262,16 @@ class FaqFormatter: pre = 0 if edit: print '<P>' - emit(faqconf.ENTRY_FOOTER, self.entry) - if self.entry.last_changed_date: - emit(faqconf.ENTRY_LOGINFO, self.entry) + emit(ENTRY_FOOTER, self) + if self.last_changed_date: + emit(ENTRY_LOGINFO, self) 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: 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): self.__dir = dir @@ -279,8 +322,17 @@ class FaqDir: def show(self, file, edit=1): self.open(file).show(edit=edit) - def new(self, sec): - XXX + def new(self, section): + 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: @@ -289,13 +341,13 @@ class FaqWizard: self.dir = FaqDir() def go(self): - print "Content-type: text/html" - req = self.ui.req or "home" + print 'Content-type: text/html' + req = self.ui.req or 'home' mname = 'do_%s' % req try: meth = getattr(self, mname) except AttributeError: - self.error("Bad request %s" % `req`) + self.error("Bad request type %s." % `req`) else: try: meth() @@ -303,29 +355,43 @@ class FaqWizard: self.error("Invalid entry file name %s" % exc.file) except NoSuchFile, exc: self.error("No entry with file name %s" % exc.file) + except NoSuchSection, exc: + self.error("No section number %s" % exc.section) self.epilogue() def error(self, message, **kw): - self.prologue(faqconf.T_ERROR) - apply(emit, (message,), kw) + self.prologue(T_ERROR) + emit(message, 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): - emit(faqconf.EPILOGUE) + emit(EPILOGUE) def do_home(self): - self.prologue(faqconf.T_HOME) - emit(faqconf.HOME) + self.prologue(T_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): query = self.ui.query if not query: - self.error("No query string") + self.error("Empty query string!") return - self.prologue(faqconf.T_SEARCH) - if self.ui.casefold == "no": + self.prologue(T_SEARCH) + 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) else: p = regex.compile(query, regex.casefold) @@ -338,26 +404,26 @@ class FaqWizard: if p.search(entry.title) >= 0 or p.search(entry.body) >= 0: hits.append(file) if not hits: - emit(faqconf.NO_HITS, count=0) - elif len(hits) <= faqconf.MAXHITS: + emit(NO_HITS, self.ui, count=0) + elif len(hits) <= MAXHITS: if len(hits) == 1: - emit(faqconf.ONE_HIT, count=1) + emit(ONE_HIT, count=1) else: - emit(faqconf.FEW_HITS, count=len(hits)) + emit(FEW_HITS, count=len(hits)) self.format_all(hits) else: - emit(faqconf.MANY_HITS, count=len(hits)) + emit(MANY_HITS, count=len(hits)) self.format_index(hits) def do_all(self): - self.prologue(faqconf.T_ALL) + self.prologue(T_ALL) files = self.dir.list() self.last_changed(files) self.format_all(files) def do_compat(self): files = self.dir.list() - emit(faqconf.COMPAT) + emit(COMPAT) self.last_changed(files) self.format_all(files, edit=0) sys.exit(0) @@ -372,7 +438,7 @@ class FaqWizard: mtime = st[stat.ST_MTIME] if mtime > latest: latest = mtime - print time.strftime(faqconf.LAST_CHANGED, + print time.strftime(LAST_CHANGED, time.localtime(time.time())) def format_all(self, files, edit=1): @@ -380,10 +446,10 @@ class FaqWizard: self.dir.show(file, edit=edit) def do_index(self): - self.prologue(faqconf.T_INDEX) - self.format_index(self.dir.list()) + self.prologue(T_INDEX) + self.format_index(self.dir.list(), add=1) - def format_index(self, files): + def format_index(self, files, add=0): sec = 0 for file in files: try: @@ -392,14 +458,16 @@ class FaqWizard: continue if entry.sec != sec: if sec: - emit(faqconf.INDEX_ENDSECTION, sec=sec) + if add: + emit(INDEX_ADDSECTION, sec=sec) + emit(INDEX_ENDSECTION, sec=sec) sec = entry.sec - emit(faqconf.INDEX_SECTION, - sec=sec, - title=faqconf.SECTION_TITLES[sec]) - emit(faqconf.INDEX_ENTRY, entry) + emit(INDEX_SECTION, sec=sec, title=SECTION_TITLES[sec]) + emit(INDEX_ENTRY, entry) if sec: - emit(faqconf.INDEX_ENDSECTION, sec=sec) + if add: + emit(INDEX_ADDSECTION, sec=sec) + emit(INDEX_ENDSECTION, sec=sec) def do_recent(self): if not self.ui.days: @@ -422,53 +490,58 @@ class FaqWizard: list.append((mtime, file)) list.sort() list.reverse() - self.prologue(faqconf.T_RECENT) + self.prologue(T_RECENT) if days <= 1: period = "%.2g hours" % (days*24) else: period = "%.6g days" % days if not list: - emit(faqconf.NO_RECENT, period=period) + emit(NO_RECENT, period=period) elif len(list) == 1: - emit(faqconf.ONE_RECENT, period=period) + emit(ONE_RECENT, period=period) 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)) - emit(faqconf.TAIL_RECENT) + emit(TAIL_RECENT) def do_roulette(self): - self.prologue(faqconf.T_ROULETTE) + self.prologue(T_ROULETTE) file = self.dir.roulette() self.dir.show(file) def do_help(self): - self.prologue(faqconf.T_HELP) - emit(faqconf.HELP) + self.prologue(T_HELP) + emit(HELP) def do_show(self): entry = self.dir.open(self.ui.file) - self.prologue("Python FAQ Entry") + self.prologue(T_SHOW) entry.show() def do_add(self): 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): self.prologue(T_DELETE) - self.error("Not yet implemented") + emit(DELETE) def do_log(self): entry = self.dir.open(self.ui.file) - self.prologue(faqconf.T_LOG, entry) - emit(faqconf.LOG, entry) - self.rlog(interpolate(faqconf.SH_RLOG, entry), entry) + self.prologue(T_LOG, entry) + emit(LOG, entry) + self.rlog(interpolate(SH_RLOG, entry), entry) def rlog(self, command, entry=None): output = os.popen(command).read() - sys.stdout.write("<PRE>") + sys.stdout.write('<PRE>') athead = 0 - lines = string.split(output, "\n") + lines = string.split(output, '\n') while lines and not lines[-1]: del lines[-1] if lines: @@ -479,8 +552,8 @@ class FaqWizard: for line in lines: if entry and athead and line[:9] == 'revision ': rev = string.strip(line[9:]) - if rev != "1.1": - emit(faqconf.DIFFLINK, entry, rev=rev, line=line) + if rev != '1.1': + emit(DIFFLINK, entry, rev=rev, line=line) else: print line athead = 0 @@ -489,61 +562,76 @@ class FaqWizard: if line[:1] == '-' and len(line) >= 20 and \ line == len(line) * line[0]: athead = 1 - sys.stdout.write("<HR>") + sys.stdout.write('<HR>') else: print line - print "</PRE>" + print '</PRE>' def do_diff(self): entry = self.dir.open(self.ui.file) rev = self.ui.rev 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: - self.error("Invalid revision number: %s" % `rev`) + self.error("Invalid revision number: %s." % `rev`) [major, minor] = map(string.atoi, r.group(1, 2)) if minor == 1: - self.error("No previous revision") + self.error("No previous revision.") return - prev = "%d.%d" % (major, minor-1) - self.prologue(faqconf.T_DIFF, entry) - self.shell(interpolate(faqconf.SH_RDIFF, entry, rev=rev, prev=prev)) + prev = '%d.%d' % (major, minor-1) + self.prologue(T_DIFF, entry) + self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev)) def shell(self, command): output = os.popen(command).read() - sys.stdout.write("<PRE>") + sys.stdout.write('<PRE>') print escape(output) - print "</PRE>" + print '</PRE>' def do_new(self): - editor = FaqEditor(self.ui, self.dir.new(self.file)) - self.prologue(faqconf.T_NEW) - self.error("Not yet implemented") + entry = self.dir.new(section=string.atoi(self.ui.section)) + entry.version = '*new*' + 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): entry = self.dir.open(self.ui.file) entry.load_version() - self.prologue(faqconf.T_EDIT) - emit(faqconf.EDITHEAD) - emit(faqconf.EDITFORM1, entry, editversion=entry.version) - emit(faqconf.EDITFORM2, entry, load_my_cookie(), log=self.ui.log) - emit(faqconf.EDITFORM3) + 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_review(self): - entry = self.dir.open(self.ui.file) - entry.load_version() + 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.load_version() # Check that the FAQ entry number didn't change 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 # Check that the edited version is the current version if entry.version != self.ui.editversion: - self.error("Version conflict.") - emit(faqconf.VERSIONCONFLICT, entry, self.ui) + self.error("Commit version conflict!") + emit(VERSIONCONFLICT, entry, self.ui) return - commit_ok = ((not faqconf.PASSWORD - or self.ui.password == faqconf.PASSWORD) + commit_ok = ((not PASSWORD + or self.ui.password == PASSWORD) and self.ui.author and '@' in self.ui.email and self.ui.log) @@ -551,40 +639,45 @@ class FaqWizard: if not commit_ok: self.cantcommit() else: - self.commit() + self.commit(entry) return - self.prologue(faqconf.T_REVIEW) - emit(faqconf.REVIEWHEAD) + self.prologue(T_REVIEW) + emit(REVIEWHEAD) entry.body = self.ui.body entry.title = self.ui.title entry.show(edit=0) - emit(faqconf.EDITFORM1, entry, self.ui) + emit(EDITFORM1, self.ui, entry) if commit_ok: - emit(faqconf.COMMIT) + emit(COMMIT) else: - emit(faqconf.NOCOMMIT) - emit(faqconf.EDITFORM2, entry, load_my_cookie(), log=self.ui.log) - emit(faqconf.EDITFORM3) + emit(NOCOMMIT) + emit(EDITFORM2, self.ui, entry, load_my_cookie()) + emit(EDITFORM3) def cantcommit(self): - self.prologue(faqconf.T_CANTCOMMIT) - print faqconf.CANTCOMMIT_HEAD + self.prologue(T_CANTCOMMIT) + print CANTCOMMIT_HEAD if not self.ui.passwd: - emit(faqconf.NEED_PASSWD) + emit(NEED_PASSWD) if not self.ui.log: - emit(faqconf.NEED_LOG) + emit(NEED_LOG) if not self.ui.author: - emit(faqconf.NEED_AUTHOR) + emit(NEED_AUTHOR) if not self.ui.email: - emit(faqconf.NEED_EMAIL) - print faqconf.CANTCOMMIT_TAIL - - def commit(self): - file = self.ui.file - entry = self.dir.open(file) - # Chech that there were any changes + emit(NEED_EMAIL) + print CANTCOMMIT_TAIL + + def commit(self, entry): + file = entry.file + # Normalize line endings in body + 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: - self.error("No changes.") + self.error("You didn't make any changes!") return # XXX Should lock here try: @@ -592,25 +685,25 @@ class FaqWizard: except os.error: pass try: - f = open(file, "w") + f = open(file, 'w') except IOError, why: - self.error(faqconf.CANTWRITE, file=file, why=why) + self.error(CANTWRITE, file=file, why=why) return date = time.ctime(time.time()) - emit(faqconf.FILEHEADER, self.ui, os.environ, date=date, file=f) - f.write("\n") + emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0) + f.write('\n') f.write(self.ui.body) - f.write("\n") + f.write('\n') f.close() import tempfile tfn = tempfile.mktemp() - f = open(tfn, "w") - emit(faqconf.LOGHEADER, self.ui, os.environ, date=date, file=f) + f = open(tfn, 'w') + emit(LOGHEADER, self.ui, os.environ, date=date, _file=f) f.close() command = interpolate( - faqconf.SH_LOCK + "\n" + faqconf.SH_CHECKIN, + SH_LOCK + '\n' + SH_CHECKIN, file=file, tfn=tfn) p = os.popen(command) @@ -618,12 +711,12 @@ class FaqWizard: sts = p.close() # XXX Should unlock here if not sts: - self.prologue(faqconf.T_COMMITTED) - emit(faqconf.COMMITTED) + self.prologue(T_COMMITTED) + emit(COMMITTED) else: - self.error(faqconf.T_COMMITFAILED) - emit(faqconf.COMMITFAILED, sts=sts) - print "<PRE>%s</PRE>" % cgi.escape(output) + self.error(T_COMMITFAILED) + emit(COMMITFAILED, sts=sts) + print '<PRE>%s</PRE>' % escape(output) try: os.unlink(tfn) @@ -636,31 +729,17 @@ class FaqWizard: wiz = FaqWizard() 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 = """\ #! /usr/local/bin/python FAQDIR = "/usr/people/guido/python/FAQ" - -# 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 +import sys, os os.chdir(FAQDIR) sys.path.insert(0, FAQDIR) -try: - 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)) +import faqwiz """ |