home *** CD-ROM | disk | FTP | other *** search
/ MacHack 2000 / MacHack 2000.toast / pc / The Hacks / MacHacksBug / Python 1.5.2c1 / Tools / faqwiz / faqwiz.py < prev    next >
Encoding:
Python Source  |  2000-06-23  |  24.0 KB  |  828 lines

  1. """Generic FAQ Wizard.
  2.  
  3. This is a CGI program that maintains a user-editable FAQ.  It uses RCS
  4. to keep track of changes to individual FAQ entries.  It is fully
  5. configurable; everything you might want to change when using this
  6. program to maintain some other FAQ than the Python FAQ is contained in
  7. the configuration module, faqconf.py.
  8.  
  9. Note that this is not an executable script; it's an importable module.
  10. The actual script to place in cgi-bin is faqw.py.
  11.  
  12. """
  13.  
  14. import sys, string, time, os, stat, re, cgi, faqconf
  15. from faqconf import *                   # This imports all uppercase names
  16. now = time.time()
  17.  
  18. class FileError:
  19.     def __init__(self, file):
  20.         self.file = file
  21.  
  22. class InvalidFile(FileError):
  23.     pass
  24.  
  25. class NoSuchSection(FileError):
  26.     def __init__(self, section):
  27.         FileError.__init__(self, NEWFILENAME %(section, 1))
  28.         self.section = section
  29.  
  30. class NoSuchFile(FileError):
  31.     def __init__(self, file, why=None):
  32.         FileError.__init__(self, file)
  33.         self.why = why
  34.  
  35. def escape(s):
  36.     s = string.replace(s, '&', '&')
  37.     s = string.replace(s, '<', '<')
  38.     s = string.replace(s, '>', '>')
  39.     return s
  40.  
  41. def escapeq(s):
  42.     s = escape(s)
  43.     s = string.replace(s, '"', '"')
  44.     return s
  45.  
  46. def _interpolate(format, args, kw):
  47.     try:
  48.         quote = kw['_quote']
  49.     except KeyError:
  50.         quote = 1
  51.     d = (kw,) + args + (faqconf.__dict__,)
  52.     m = MagicDict(d, quote)
  53.     return format % m
  54.  
  55. def interpolate(format, *args, **kw):
  56.     return _interpolate(format, args, kw)
  57.  
  58. def emit(format, *args, **kw):
  59.     try:
  60.         f = kw['_file']
  61.     except KeyError:
  62.         f = sys.stdout
  63.     f.write(_interpolate(format, args, kw))
  64.  
  65. translate_prog = None
  66.  
  67. def translate(text, pre=0):
  68.     global translate_prog
  69.     if not translate_prog:
  70.         translate_prog = prog = re.compile(
  71.             r'\b(http|ftp|https)://\S+(\b|/)|\b[-.\w]+@[-.\w]+')
  72.     else:
  73.         prog = translate_prog
  74.     i = 0
  75.     list = []
  76.     while 1:
  77.         m = prog.search(text, i)
  78.         if not m:
  79.             break
  80.         j = m.start()
  81.         list.append(escape(text[i:j]))
  82.         i = j
  83.         url = m.group(0)
  84.         while url[-1] in '();:,.?\'"<>':
  85.             url = url[:-1]
  86.         i = i + len(url)
  87.         url = escape(url)
  88.         if not pre or (pre and PROCESS_PREFORMAT):
  89.             if ':' in url:
  90.                 repl = '<A HREF="%s">%s</A>' % (url, url)
  91.             else:
  92.                 repl = '<A HREF="mailto:%s">%s</A>' % (url, url)
  93.         else:
  94.             repl = url
  95.         list.append(repl)
  96.     j = len(text)
  97.     list.append(escape(text[i:j]))
  98.     return string.join(list, '')
  99.  
  100. def emphasize(line):
  101.     return re.sub(r'\*([a-zA-Z]+)\*', r'<I>\1</I>', line)
  102.  
  103. revparse_prog = None
  104.  
  105. def revparse(rev):
  106.     global revparse_prog
  107.     if not revparse_prog:
  108.         revparse_prog = re.compile(r'^(\d{1,3})\.(\d{1,4})$')
  109.     m = revparse_prog.match(rev)
  110.     if not m:
  111.         return None
  112.     [major, minor] = map(string.atoi, m.group(1, 2))
  113.     return major, minor
  114.  
  115. def load_cookies():
  116.     if not os.environ.has_key('HTTP_COOKIE'):
  117.         return {}
  118.     raw = os.environ['HTTP_COOKIE']
  119.     words = map(string.strip, string.split(raw, ';'))
  120.     cookies = {}
  121.     for word in words:
  122.         i = string.find(word, '=')
  123.         if i >= 0:
  124.             key, value = word[:i], word[i+1:]
  125.             cookies[key] = value
  126.     return cookies
  127.  
  128. def load_my_cookie():
  129.     cookies = load_cookies()
  130.     try:
  131.         value = cookies[COOKIE_NAME]
  132.     except KeyError:
  133.         return {}
  134.     import urllib
  135.     value = urllib.unquote(value)
  136.     words = string.split(value, '/')
  137.     while len(words) < 3:
  138.         words.append('')
  139.     author = string.join(words[:-2], '/')
  140.     email = words[-2]
  141.     password = words[-1]
  142.     return {'author': author,
  143.             'email': email,
  144.             'password': password}
  145.  
  146. def send_my_cookie(ui):
  147.     name = COOKIE_NAME
  148.     value = "%s/%s/%s" % (ui.author, ui.email, ui.password)
  149.     import urllib
  150.     value = urllib.quote(value)
  151.     then = now + COOKIE_LIFETIME
  152.     gmt = time.gmtime(then)
  153.     path = os.environ.get('SCRIPT_NAME', '/cgi-bin/')
  154.     print "Set-Cookie: %s=%s; path=%s;" % (name, value, path),
  155.     print time.strftime("expires=%a, %d-%b-%y %X GMT", gmt)
  156.  
  157. class MagicDict:
  158.  
  159.     def __init__(self, d, quote):
  160.         self.__d = d
  161.         self.__quote = quote
  162.  
  163.     def __getitem__(self, key):
  164.         for d in self.__d:
  165.             try:
  166.                 value = d[key]
  167.                 if value:
  168.                     value = str(value)
  169.                     if self.__quote:
  170.                         value = escapeq(value)
  171.                     return value
  172.             except KeyError:
  173.                 pass
  174.         return ''
  175.  
  176. class UserInput:
  177.  
  178.     def __init__(self):
  179.         self.__form = cgi.FieldStorage()
  180.  
  181.     def __getattr__(self, name):
  182.         if name[0] == '_':
  183.             raise AttributeError
  184.         try:
  185.             value = self.__form[name].value
  186.         except (TypeError, KeyError):
  187.             value = ''
  188.         else:
  189.             value = string.strip(value)
  190.         setattr(self, name, value)
  191.         return value
  192.  
  193.     def __getitem__(self, key):
  194.         return getattr(self, key)
  195.  
  196. class FaqEntry:
  197.  
  198.     def __init__(self, fp, file, sec_num):
  199.         self.file = file
  200.         self.sec, self.num = sec_num
  201.         if fp:
  202.             import rfc822
  203.             self.__headers = rfc822.Message(fp)
  204.             self.body = string.strip(fp.read())
  205.         else:
  206.             self.__headers = {'title': "%d.%d. " % sec_num}
  207.             self.body = ''
  208.  
  209.     def __getattr__(self, name):
  210.         if name[0] == '_':
  211.             raise AttributeError
  212.         key = string.join(string.split(name, '_'), '-')
  213.         try:
  214.             value = self.__headers[key]
  215.         except KeyError:
  216.             value = ''
  217.         setattr(self, name, value)
  218.         return value
  219.  
  220.     def __getitem__(self, key):
  221.         return getattr(self, key)
  222.  
  223.     def load_version(self):
  224.         command = interpolate(SH_RLOG_H, self)
  225.         p = os.popen(command)
  226.         version = ''
  227.         while 1:
  228.             line = p.readline()
  229.             if not line:
  230.                 break
  231.             if line[:5] == 'head:':
  232.                 version = string.strip(line[5:])
  233.         p.close()
  234.         self.version = version
  235.  
  236.     def getmtime(self):
  237.         if not self.last_changed_date:
  238.             return 0
  239.         try:
  240.             return os.stat(self.file)[stat.ST_MTIME]
  241.         except os.error:
  242.             return 0
  243.  
  244.     def emit_marks(self):
  245.         mtime = self.getmtime()
  246.         if mtime >= now - DT_VERY_RECENT:
  247.             emit(MARK_VERY_RECENT, self)
  248.         elif mtime >= now - DT_RECENT:
  249.             emit(MARK_RECENT, self)
  250.  
  251.     def show(self, edit=1):
  252.         emit(ENTRY_HEADER1, self)
  253.         self.emit_marks()
  254.         emit(ENTRY_HEADER2, self)
  255.         pre = 0
  256.         raw = 0
  257.         for line in string.split(self.body, '\n'):
  258.             # Allow the user to insert raw html into a FAQ answer
  259.             # (Skip Montanaro, with changes by Guido)
  260.             tag = string.lower(string.rstrip(line))
  261.             if tag == '<html>':
  262.                 raw = 1
  263.                 continue
  264.             if tag == '</html>':
  265.                 raw = 0
  266.                 continue
  267.             if raw:
  268.                 print line
  269.                 continue
  270.             if not string.strip(line):
  271.                 if pre:
  272.                     print '</PRE>'
  273.                     pre = 0
  274.                 else:
  275.                     print '<P>'
  276.             else:
  277.                 if line[0] not in string.whitespace:
  278.                     if pre:
  279.                         print '</PRE>'
  280.                         pre = 0
  281.                 else:
  282.                     if not pre:
  283.                         print '<PRE>'
  284.                         pre = 1
  285.                 if '/' in line or '@' in line:
  286.                     line = translate(line, pre)
  287.                 elif '<' in line or '&' in line:
  288.                     line = escape(line)
  289.                 if not pre and '*' in line:
  290.                     line = emphasize(line)
  291.                 print line
  292.         if pre:
  293.             print '</PRE>'
  294.             pre = 0
  295.         if edit:
  296.             print '<P>'
  297.             emit(ENTRY_FOOTER, self)
  298.             if self.last_changed_date:
  299.                 emit(ENTRY_LOGINFO, self)
  300.         print '<P>'
  301.  
  302. class FaqDir:
  303.  
  304.     entryclass = FaqEntry
  305.  
  306.     __okprog = re.compile(OKFILENAME)
  307.  
  308.     def __init__(self, dir=os.curdir):
  309.         self.__dir = dir
  310.         self.__files = None
  311.  
  312.     def __fill(self):
  313.         if self.__files is not None:
  314.             return
  315.         self.__files = files = []
  316.         okprog = self.__okprog
  317.         for file in os.listdir(self.__dir):
  318.             if self.__okprog.match(file):
  319.                 files.append(file)
  320.         files.sort()
  321.  
  322.     def good(self, file):
  323.         return self.__okprog.match(file)
  324.  
  325.     def parse(self, file):
  326.         m = self.good(file)
  327.         if not m:
  328.             return None
  329.         sec, num = m.group(1, 2)
  330.         return string.atoi(sec), string.atoi(num)
  331.  
  332.     def list(self):
  333.         # XXX Caller shouldn't modify result
  334.         self.__fill()
  335.         return self.__files
  336.  
  337.     def open(self, file):
  338.         sec_num = self.parse(file)
  339.         if not sec_num:
  340.             raise InvalidFile(file)
  341.         try:
  342.             fp = open(file)
  343.         except IOError, msg:
  344.             raise NoSuchFile(file, msg)
  345.         try:
  346.             return self.entryclass(fp, file, sec_num)
  347.         finally:
  348.             fp.close()
  349.  
  350.     def show(self, file, edit=1):
  351.         self.open(file).show(edit=edit)
  352.  
  353.     def new(self, section):
  354.         if not SECTION_TITLES.has_key(section):
  355.             raise NoSuchSection(section)
  356.         maxnum = 0
  357.         for file in self.list():
  358.             sec, num = self.parse(file)
  359.             if sec == section:
  360.                 maxnum = max(maxnum, num)
  361.         sec_num = (section, maxnum+1)
  362.         file = NEWFILENAME % sec_num
  363.         return self.entryclass(None, file, sec_num)
  364.  
  365. class FaqWizard:
  366.  
  367.     def __init__(self):
  368.         self.ui = UserInput()
  369.         self.dir = FaqDir()
  370.  
  371.     def go(self):
  372.         print 'Content-type: text/html'
  373.         req = self.ui.req or 'home'
  374.         mname = 'do_%s' % req
  375.         try:
  376.             meth = getattr(self, mname)
  377.         except AttributeError:
  378.             self.error("Bad request type %s." % `req`)
  379.         else:
  380.             try:
  381.                 meth()
  382.             except InvalidFile, exc:
  383.                 self.error("Invalid entry file name %s" % exc.file)
  384.             except NoSuchFile, exc:
  385.                 self.error("No entry with file name %s" % exc.file)
  386.             except NoSuchSection, exc:
  387.                 self.error("No section number %s" % exc.section)
  388.         self.epilogue()
  389.  
  390.     def error(self, message, **kw):
  391.         self.prologue(T_ERROR)
  392.         emit(message, kw)
  393.  
  394.     def prologue(self, title, entry=None, **kw):
  395.         emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
  396.  
  397.     def epilogue(self):
  398.         emit(EPILOGUE)
  399.  
  400.     def do_home(self):
  401.         self.prologue(T_HOME)
  402.         emit(HOME)
  403.  
  404.     def do_debug(self):
  405.         self.prologue("FAQ Wizard Debugging")
  406.         form = cgi.FieldStorage()
  407.         cgi.print_form(form)
  408.         cgi.print_environ(os.environ)
  409.         cgi.print_directory()
  410.         cgi.print_arguments()
  411.  
  412.     def do_search(self):
  413.         query = self.ui.query
  414.         if not query:
  415.             self.error("Empty query string!")
  416.             return
  417.         if self.ui.querytype == 'simple':
  418.             query = re.escape(query)
  419.             queries = [query]
  420.         elif self.ui.querytype in ('anykeywords', 'allkeywords'):
  421.             words = filter(None, re.split('\W+', query))
  422.             if not words:
  423.                 self.error("No keywords specified!")
  424.                 return
  425.             words = map(lambda w: r'\b%s\b' % w, words)
  426.             if self.ui.querytype[:3] == 'any':
  427.                 queries = [string.join(words, '|')]
  428.             else:
  429.                 # Each of the individual queries must match
  430.                 queries = words
  431.         else:
  432.             # Default to regular expression
  433.             queries = [query]
  434.         self.prologue(T_SEARCH)
  435.         progs = []
  436.         for query in queries:
  437.             if self.ui.casefold == 'no':
  438.                 p = re.compile(query)
  439.             else:
  440.                 p = re.compile(query, re.IGNORECASE)
  441.             progs.append(p)
  442.         hits = []
  443.         for file in self.dir.list():
  444.             try:
  445.                 entry = self.dir.open(file)
  446.             except FileError:
  447.                 constants
  448.             for p in progs:
  449.                 if not p.search(entry.title) and not p.search(entry.body):
  450.                     break
  451.             else:
  452.                 hits.append(file)
  453.         if not hits:
  454.             emit(NO_HITS, self.ui, count=0)
  455.         elif len(hits) <= MAXHITS:
  456.             if len(hits) == 1:
  457.                 emit(ONE_HIT, count=1)
  458.             else:
  459.                 emit(FEW_HITS, count=len(hits))
  460.             self.format_all(hits, headers=0)
  461.         else:
  462.             emit(MANY_HITS, count=len(hits))
  463.             self.format_index(hits)
  464.  
  465.     def do_all(self):
  466.         self.prologue(T_ALL)
  467.         files = self.dir.list()
  468.         self.last_changed(files)
  469.         self.format_index(files, localrefs=1)
  470.         self.format_all(files)
  471.  
  472.     def do_compat(self):
  473.         files = self.dir.list()
  474.         emit(COMPAT)
  475.         self.last_changed(files)
  476.         self.format_index(files, localrefs=1)
  477.         self.format_all(files, edit=0)
  478.         sys.exit(0)                     # XXX Hack to suppress epilogue
  479.  
  480.     def last_changed(self, files):
  481.         latest = 0
  482.         for file in files:
  483.             entry = self.dir.open(file)
  484.             if entry:
  485.                 mtime = mtime = entry.getmtime()
  486.                 if mtime > latest:
  487.                     latest = mtime
  488.         print time.strftime(LAST_CHANGED, time.localtime(latest))
  489.         emit(EXPLAIN_MARKS)
  490.  
  491.     def format_all(self, files, edit=1, headers=1):
  492.         sec = 0
  493.         for file in files:
  494.             try:
  495.                 entry = self.dir.open(file)
  496.             except NoSuchFile:
  497.                 continue
  498.             if headers and entry.sec != sec:
  499.                 sec = entry.sec
  500.                 try:
  501.                     title = SECTION_TITLES[sec]
  502.                 except KeyError:
  503.                     title = "Untitled"
  504.                 emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n",
  505.                      sec=sec, title=title)
  506.             entry.show(edit=edit)
  507.  
  508.     def do_index(self):
  509.         self.prologue(T_INDEX)
  510.         files = self.dir.list()
  511.         self.last_changed(files)
  512.         self.format_index(files, add=1)
  513.  
  514.     def format_index(self, files, add=0, localrefs=0):
  515.         sec = 0
  516.         for file in files:
  517.             try:
  518.                 entry = self.dir.open(file)
  519.             except NoSuchFile:
  520.                 continue
  521.             if entry.sec != sec:
  522.                 if sec:
  523.                     if add:
  524.                         emit(INDEX_ADDSECTION, sec=sec)
  525.                     emit(INDEX_ENDSECTION, sec=sec)
  526.                 sec = entry.sec
  527.                 try:
  528.                     title = SECTION_TITLES[sec]
  529.                 except KeyError:
  530.                     title = "Untitled"
  531.                 emit(INDEX_SECTION, sec=sec, title=title)
  532.             if localrefs:
  533.                 emit(LOCAL_ENTRY, entry)
  534.             else:
  535.                 emit(INDEX_ENTRY, entry)
  536.             entry.emit_marks()
  537.         if sec:
  538.             if add:
  539.                 emit(INDEX_ADDSECTION, sec=sec)
  540.             emit(INDEX_ENDSECTION, sec=sec)
  541.  
  542.     def do_recent(self):
  543.         if not self.ui.days:
  544.             days = 1
  545.         else:
  546.             days = string.atof(self.ui.days)
  547.         try:
  548.             cutoff = now - days * 24 * 3600
  549.         except OverflowError:
  550.             cutoff = 0
  551.         list = []
  552.         for file in self.dir.list():
  553.             entry = self.dir.open(file)
  554.             if not entry:
  555.                 continue
  556.             mtime = entry.getmtime()
  557.             if mtime >= cutoff:
  558.                 list.append((mtime, file))
  559.         list.sort()
  560.         list.reverse()
  561.         self.prologue(T_RECENT)
  562.         if days <= 1:
  563.             period = "%.2g hours" % (days*24)
  564.         else:
  565.             period = "%.6g days" % days
  566.         if not list:
  567.             emit(NO_RECENT, period=period)
  568.         elif len(list) == 1:
  569.             emit(ONE_RECENT, period=period)
  570.         else:
  571.             emit(SOME_RECENT, period=period, count=len(list))
  572.         self.format_all(map(lambda (mtime, file): file, list), headers=0)
  573.         emit(TAIL_RECENT)
  574.  
  575.     def do_roulette(self):
  576.         import random
  577.         files = self.dir.list()
  578.         if not files: 
  579.             self.error("No entries.")
  580.             return
  581.         file = random.choice(files)
  582.         self.prologue(T_ROULETTE)
  583.         emit(ROULETTE)
  584.         self.dir.show(file)
  585.  
  586.     def do_help(self):
  587.         self.prologue(T_HELP)
  588.         emit(HELP)
  589.  
  590.     def do_show(self):
  591.         entry = self.dir.open(self.ui.file)
  592.         self.prologue(T_SHOW)
  593.         entry.show()
  594.  
  595.     def do_add(self):
  596.         self.prologue(T_ADD)
  597.         emit(ADD_HEAD)
  598.         sections = SECTION_TITLES.items()
  599.         sections.sort()
  600.         for section, title in sections:
  601.             emit(ADD_SECTION, section=section, title=title)
  602.         emit(ADD_TAIL)
  603.  
  604.     def do_delete(self):
  605.         self.prologue(T_DELETE)
  606.         emit(DELETE)
  607.  
  608.     def do_log(self):
  609.         entry = self.dir.open(self.ui.file)
  610.         self.prologue(T_LOG, entry)
  611.         emit(LOG, entry)
  612.         self.rlog(interpolate(SH_RLOG, entry), entry)
  613.  
  614.     def rlog(self, command, entry=None):
  615.         output = os.popen(command).read()
  616.         sys.stdout.write('<PRE>')
  617.         athead = 0
  618.         lines = string.split(output, '\n')
  619.         while lines and not lines[-1]:
  620.             del lines[-1]
  621.         if lines:
  622.             line = lines[-1]
  623.             if line[:1] == '=' and len(line) >= 40 and \
  624.                line == line[0]*len(line):
  625.                 del lines[-1]
  626.         headrev = None
  627.         for line in lines:
  628.             if entry and athead and line[:9] == 'revision ':
  629.                 rev = string.strip(line[9:])
  630.                 mami = revparse(rev)
  631.                 if not mami:
  632.                     print line
  633.                 else:
  634.                     emit(REVISIONLINK, entry, rev=rev, line=line)
  635.                     if mami[1] > 1:
  636.                         prev = "%d.%d" % (mami[0], mami[1]-1)
  637.                         emit(DIFFLINK, entry, prev=prev, rev=rev)
  638.                     if headrev:
  639.                         emit(DIFFLINK, entry, prev=rev, rev=headrev)
  640.                     else:
  641.                         headrev = rev
  642.                     print
  643.                 athead = 0
  644.             else:
  645.                 athead = 0
  646.                 if line[:1] == '-' and len(line) >= 20 and \
  647.                    line == len(line) * line[0]:
  648.                     athead = 1
  649.                     sys.stdout.write('<HR>')
  650.                 else:
  651.                     print line
  652.         print '</PRE>'
  653.  
  654.     def do_revision(self):
  655.         entry = self.dir.open(self.ui.file)
  656.         rev = self.ui.rev
  657.         mami = revparse(rev)
  658.         if not mami:
  659.             self.error("Invalid revision number: %s." % `rev`)
  660.         self.prologue(T_REVISION, entry)
  661.         self.shell(interpolate(SH_REVISION, entry, rev=rev))
  662.  
  663.     def do_diff(self):
  664.         entry = self.dir.open(self.ui.file)
  665.         prev = self.ui.prev
  666.         rev = self.ui.rev
  667.         mami = revparse(rev)
  668.         if not mami:
  669.             self.error("Invalid revision number: %s." % `rev`)
  670.         if prev:
  671.             if not revparse(prev):
  672.                 self.error("Invalid previous revision number: %s." % `prev`)
  673.         else:
  674.             prev = '%d.%d' % (mami[0], mami[1])
  675.         self.prologue(T_DIFF, entry)
  676.         self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
  677.  
  678.     def shell(self, command):
  679.         output = os.popen(command).read()
  680.         sys.stdout.write('<PRE>')
  681.         print escape(output)
  682.         print '</PRE>'
  683.  
  684.     def do_new(self):
  685.         entry = self.dir.new(section=string.atoi(self.ui.section))
  686.         entry.version = '*new*'
  687.         self.prologue(T_EDIT)
  688.         emit(EDITHEAD)
  689.         emit(EDITFORM1, entry, editversion=entry.version)
  690.         emit(EDITFORM2, entry, load_my_cookie())
  691.         emit(EDITFORM3)
  692.         entry.show(edit=0)
  693.  
  694.     def do_edit(self):
  695.         entry = self.dir.open(self.ui.file)
  696.         entry.load_version()
  697.         self.prologue(T_EDIT)
  698.         emit(EDITHEAD)
  699.         emit(EDITFORM1, entry, editversion=entry.version)
  700.         emit(EDITFORM2, entry, load_my_cookie())
  701.         emit(EDITFORM3)
  702.         entry.show(edit=0)
  703.  
  704.     def do_review(self):
  705.         send_my_cookie(self.ui)
  706.         if self.ui.editversion == '*new*':
  707.             sec, num = self.dir.parse(self.ui.file)
  708.             entry = self.dir.new(section=sec)
  709.             entry.version = "*new*"
  710.             if entry.file != self.ui.file:
  711.                 self.error("Commit version conflict!")
  712.                 emit(NEWCONFLICT, self.ui, sec=sec, num=num)
  713.                 return
  714.         else:
  715.             entry = self.dir.open(self.ui.file)
  716.             entry.load_version()
  717.         # Check that the FAQ entry number didn't change
  718.         if string.split(self.ui.title)[:1] != string.split(entry.title)[:1]:
  719.             self.error("Don't change the entry number please!")
  720.             return
  721.         # Check that the edited version is the current version
  722.         if entry.version != self.ui.editversion:
  723.             self.error("Commit version conflict!")
  724.             emit(VERSIONCONFLICT, entry, self.ui)
  725.             return
  726.         commit_ok = ((not PASSWORD
  727.                       or self.ui.password == PASSWORD) 
  728.                      and self.ui.author
  729.                      and '@' in self.ui.email
  730.                      and self.ui.log)
  731.         if self.ui.commit:
  732.             if not commit_ok:
  733.                 self.cantcommit()
  734.             else:
  735.                 self.commit(entry)
  736.             return
  737.         self.prologue(T_REVIEW)
  738.         emit(REVIEWHEAD)
  739.         entry.body = self.ui.body
  740.         entry.title = self.ui.title
  741.         entry.show(edit=0)
  742.         emit(EDITFORM1, self.ui, entry)
  743.         if commit_ok:
  744.             emit(COMMIT)
  745.         else:
  746.             emit(NOCOMMIT_HEAD)
  747.             self.errordetail()
  748.             emit(NOCOMMIT_TAIL)
  749.         emit(EDITFORM2, self.ui, entry, load_my_cookie())
  750.         emit(EDITFORM3)
  751.  
  752.     def cantcommit(self):
  753.         self.prologue(T_CANTCOMMIT)
  754.         print CANTCOMMIT_HEAD
  755.         self.errordetail()
  756.         print CANTCOMMIT_TAIL
  757.  
  758.     def errordetail(self):
  759.         if PASSWORD and self.ui.password != PASSWORD:
  760.             emit(NEED_PASSWD)
  761.         if not self.ui.log:
  762.             emit(NEED_LOG)
  763.         if not self.ui.author:
  764.             emit(NEED_AUTHOR)
  765.         if not self.ui.email:
  766.             emit(NEED_EMAIL)
  767.  
  768.     def commit(self, entry):
  769.         file = entry.file
  770.         # Normalize line endings in body
  771.         if '\r' in self.ui.body:
  772.             self.ui.body = re.sub('\r\n?', '\n', self.ui.body)
  773.         # Normalize whitespace in title
  774.         self.ui.title = string.join(string.split(self.ui.title))
  775.         # Check that there were any changes
  776.         if self.ui.body == entry.body and self.ui.title == entry.title:
  777.             self.error("You didn't make any changes!")
  778.             return
  779.         # XXX Should lock here
  780.         try:
  781.             os.unlink(file)
  782.         except os.error:
  783.             pass
  784.         try:
  785.             f = open(file, 'w')
  786.         except IOError, why:
  787.             self.error(CANTWRITE, file=file, why=why)
  788.             return
  789.         date = time.ctime(now)
  790.         emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0)
  791.         f.write('\n')
  792.         f.write(self.ui.body)
  793.         f.write('\n')
  794.         f.close()
  795.  
  796.         import tempfile
  797.         tfn = tempfile.mktemp()
  798.         f = open(tfn, 'w')
  799.         emit(LOGHEADER, self.ui, os.environ, date=date, _file=f)
  800.         f.close()
  801.  
  802.         command = interpolate(
  803.             SH_LOCK + '\n' + SH_CHECKIN,
  804.             file=file, tfn=tfn)
  805.  
  806.         p = os.popen(command)
  807.         output = p.read()
  808.         sts = p.close()
  809.         # XXX Should unlock here
  810.         if not sts:
  811.             self.prologue(T_COMMITTED)
  812.             emit(COMMITTED)
  813.         else:
  814.             self.error(T_COMMITFAILED)
  815.             emit(COMMITFAILED, sts=sts)
  816.         print '<PRE>%s</PRE>' % escape(output)
  817.  
  818.         try:
  819.             os.unlink(tfn)
  820.         except os.error:
  821.             pass
  822.  
  823.         entry = self.dir.open(file)
  824.         entry.show()
  825.  
  826. wiz = FaqWizard()
  827. wiz.go()
  828.