|
"""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 to place in cgi-bin is faqw.py. |
|
|
|
""" |
|
|
|
import sys, time, os, stat, re, cgi, faqconf |
|
from faqconf import * # This imports all uppercase names |
|
now = time.time() |
|
|
|
class FileError: |
|
def __init__(self, file): |
|
self.file = file |
|
|
|
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 escape(s): |
|
s = s.replace('&', '&') |
|
s = s.replace('<', '<') |
|
s = s.replace('>', '>') |
|
return s |
|
|
|
def escapeq(s): |
|
s = escape(s) |
|
s = s.replace('"', '"') |
|
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, *args, **kw): |
|
try: |
|
f = kw['_file'] |
|
except KeyError: |
|
f = sys.stdout |
|
f.write(_interpolate(format, args, kw)) |
|
|
|
translate_prog = None |
|
|
|
def translate(text, pre=0): |
|
global translate_prog |
|
if not translate_prog: |
|
translate_prog = prog = re.compile( |
|
r'\b(http|ftp|https)://\S+(\b|/)|\b[-.\w]+@[-.\w]+') |
|
else: |
|
prog = translate_prog |
|
i = 0 |
|
list = [] |
|
while 1: |
|
m = prog.search(text, i) |
|
if not m: |
|
break |
|
j = m.start() |
|
list.append(escape(text[i:j])) |
|
i = j |
|
url = m.group(0) |
|
while url[-1] in '();:,.?\'"<>': |
|
url = url[:-1] |
|
i = i + len(url) |
|
url = escape(url) |
|
if not pre or (pre and PROCESS_PREFORMAT): |
|
if ':' in url: |
|
repl = '<A HREF="%s">%s</A>' % (url, url) |
|
else: |
|
repl = '<A HREF="mailto:%s">%s</A>' % (url, url) |
|
else: |
|
repl = url |
|
list.append(repl) |
|
j = len(text) |
|
list.append(escape(text[i:j])) |
|
return ''.join(list) |
|
|
|
def emphasize(line): |
|
return re.sub(r'\*([a-zA-Z]+)\*', r'<I>\1</I>', line) |
|
|
|
revparse_prog = None |
|
|
|
def revparse(rev): |
|
global revparse_prog |
|
if not revparse_prog: |
|
revparse_prog = re.compile(r'^(\d{1,3})\.(\d{1,4})$') |
|
m = revparse_prog.match(rev) |
|
if not m: |
|
return None |
|
[major, minor] = map(int, m.group(1, 2)) |
|
return major, minor |
|
|
|
logon = 0 |
|
def log(text): |
|
if logon: |
|
logfile = open("logfile", "a") |
|
logfile.write(text + "\n") |
|
logfile.close() |
|
|
|
def load_cookies(): |
|
if not os.environ.has_key('HTTP_COOKIE'): |
|
return {} |
|
raw = os.environ['HTTP_COOKIE'] |
|
words = [s.strip() for s in raw.split(';')] |
|
cookies = {} |
|
for word in words: |
|
i = word.find('=') |
|
if i >= 0: |
|
key, value = word[:i], word[i+1:] |
|
cookies[key] = value |
|
return cookies |
|
|
|
def load_my_cookie(): |
|
cookies = load_cookies() |
|
try: |
|
value = cookies[COOKIE_NAME] |
|
except KeyError: |
|
return {} |
|
import urllib |
|
value = urllib.unquote(value) |
|
words = value.split('/') |
|
while len(words) < 3: |
|
words.append('') |
|
author = '/'.join(words[:-2]) |
|
email = words[-2] |
|
password = words[-1] |
|
return {'author': author, |
|
'email': email, |
|
'password': password} |
|
|
|
def send_my_cookie(ui): |
|
name = COOKIE_NAME |
|
value = "%s/%s/%s" % (ui.author, ui.email, ui.password) |
|
import urllib |
|
value = urllib.quote(value) |
|
then = now + COOKIE_LIFETIME |
|
gmt = time.gmtime(then) |
|
path = os.environ.get('SCRIPT_NAME', '/cgi-bin/') |
|
print "Set-Cookie: %s=%s; path=%s;" % (name, value, path), |
|
print time.strftime("expires=%a, %d-%b-%y %X GMT", gmt) |
|
|
|
class MagicDict: |
|
|
|
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 '' |
|
|
|
class UserInput: |
|
|
|
def __init__(self): |
|
self.__form = cgi.FieldStorage() |
|
#log("\n\nbody: " + self.body) |
|
|
|
def __getattr__(self, name): |
|
if name[0] == '_': |
|
raise AttributeError |
|
try: |
|
value = self.__form[name].value |
|
except (TypeError, KeyError): |
|
value = '' |
|
else: |
|
value = value.strip() |
|
setattr(self, name, value) |
|
return value |
|
|
|
def __getitem__(self, key): |
|
return getattr(self, key) |
|
|
|
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 = fp.read().strip() |
|
else: |
|
self.__headers = {'title': "%d.%d. " % sec_num} |
|
self.body = '' |
|
|
|
def __getattr__(self, name): |
|
if name[0] == '_': |
|
raise AttributeError |
|
key = '-'.join(name.split('_')) |
|
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 = line[5:].strip() |
|
p.close() |
|
self.version = version |
|
|
|
def getmtime(self): |
|
if not self.last_changed_date: |
|
return 0 |
|
try: |
|
return os.stat(self.file)[stat.ST_MTIME] |
|
except os.error: |
|
return 0 |
|
|
|
def emit_marks(self): |
|
mtime = self.getmtime() |
|
if mtime >= now - DT_VERY_RECENT: |
|
emit(MARK_VERY_RECENT, self) |
|
elif mtime >= now - DT_RECENT: |
|
emit(MARK_RECENT, self) |
|
|
|
def show(self, edit=1): |
|
emit(ENTRY_HEADER1, self) |
|
self.emit_marks() |
|
emit(ENTRY_HEADER2, self) |
|
pre = 0 |
|
raw = 0 |
|
for line in self.body.split('\n'): |
|
# Allow the user to insert raw html into a FAQ answer |
|
# (Skip Montanaro, with changes by Guido) |
|
tag = line.rstrip().lower() |
|
if tag == '<html>': |
|
raw = 1 |
|
continue |
|
if tag == '</html>': |
|
raw = 0 |
|
continue |
|
if raw: |
|
print line |
|
continue |
|
if not line.strip(): |
|
if pre: |
|
print '</PRE>' |
|
pre = 0 |
|
else: |
|
print '<P>' |
|
else: |
|
if not line[0].isspace(): |
|
if pre: |
|
print '</PRE>' |
|
pre = 0 |
|
else: |
|
if not pre: |
|
print '<PRE>' |
|
pre = 1 |
|
if '/' in line or '@' in line: |
|
line = translate(line, pre) |
|
elif '<' in line or '&' in line: |
|
line = escape(line) |
|
if not pre and '*' in line: |
|
line = emphasize(line) |
|
print line |
|
if pre: |
|
print '</PRE>' |
|
pre = 0 |
|
if edit: |
|
print '<P>' |
|
emit(ENTRY_FOOTER, self) |
|
if self.last_changed_date: |
|
emit(ENTRY_LOGINFO, self) |
|
print '<P>' |
|
|
|
class FaqDir: |
|
|
|
entryclass = FaqEntry |
|
|
|
__okprog = re.compile(OKFILENAME) |
|
|
|
def __init__(self, dir=os.curdir): |
|
self.__dir = dir |
|
self.__files = None |
|
|
|
def __fill(self): |
|
if self.__files is not None: |
|
return |
|
self.__files = files = [] |
|
okprog = self.__okprog |
|
for file in os.listdir(self.__dir): |
|
if self.__okprog.match(file): |
|
files.append(file) |
|
files.sort() |
|
|
|
def good(self, file): |
|
return self.__okprog.match(file) |
|
|
|
def parse(self, file): |
|
m = self.good(file) |
|
if not m: |
|
return None |
|
sec, num = m.group(1, 2) |
|
return int(sec), int(num) |
|
|
|
def list(self): |
|
# XXX Caller shouldn't modify result |
|
self.__fill() |
|
return self.__files |
|
|
|
def open(self, file): |
|
sec_num = self.parse(file) |
|
if not sec_num: |
|
raise InvalidFile(file) |
|
try: |
|
fp = open(file) |
|
except IOError, msg: |
|
raise NoSuchFile(file, msg) |
|
try: |
|
return self.entryclass(fp, file, sec_num) |
|
finally: |
|
fp.close() |
|
|
|
def show(self, file, edit=1): |
|
self.open(file).show(edit=edit) |
|
|
|
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: |
|
|
|
def __init__(self): |
|
self.ui = UserInput() |
|
self.dir = FaqDir() |
|
|
|
def go(self): |
|
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 type %r." % (req,)) |
|
else: |
|
try: |
|
meth() |
|
except InvalidFile, exc: |
|
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(T_ERROR) |
|
emit(message, kw) |
|
|
|
def prologue(self, title, entry=None, **kw): |
|
emit(PROLOGUE, entry, kwdict=kw, title=escape(title)) |
|
|
|
def epilogue(self): |
|
emit(EPILOGUE) |
|
|
|
def do_home(self): |
|
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("Empty query string!") |
|
return |
|
if self.ui.querytype == 'simple': |
|
query = re.escape(query) |
|
queries = [query] |
|
elif self.ui.querytype in ('anykeywords', 'allkeywords'): |
|
words = filter(None, re.split('\W+', query)) |
|
if not words: |
|
self.error("No keywords specified!") |
|
return |
|
words = map(lambda w: r'\b%s\b' % w, words) |
|
if self.ui.querytype[:3] == 'any': |
|
queries = ['|'.join(words)] |
|
else: |
|
# Each of the individual queries must match |
|
queries = words |
|
else: |
|
# Default to regular expression |
|
queries = [query] |
|
self.prologue(T_SEARCH) |
|
progs = [] |
|
for query in queries: |
|
if self.ui.casefold == 'no': |
|
p = re.compile(query) |
|
else: |
|
p = re.compile(query, re.IGNORECASE) |
|
progs.append(p) |
|
hits = [] |
|
for file in self.dir.list(): |
|
try: |
|
entry = self.dir.open(file) |
|
except FileError: |
|
constants |
|
for p in progs: |
|
if not p.search(entry.title) and not p.search(entry.body): |
|
break |
|
else: |
|
hits.append(file) |
|
if not hits: |
|
emit(NO_HITS, self.ui, count=0) |
|
elif len(hits) <= MAXHITS: |
|
if len(hits) == 1: |
|
emit(ONE_HIT, count=1) |
|
else: |
|
emit(FEW_HITS, count=len(hits)) |
|
self.format_all(hits, headers=0) |
|
else: |
|
emit(MANY_HITS, count=len(hits)) |
|
self.format_index(hits) |
|
|
|
def do_all(self): |
|
self.prologue(T_ALL) |
|
files = self.dir.list() |
|
self.last_changed(files) |
|
self.format_index(files, localrefs=1) |
|
self.format_all(files) |
|
|
|
def do_compat(self): |
|
files = self.dir.list() |
|
emit(COMPAT) |
|
self.last_changed(files) |
|
self.format_index(files, localrefs=1) |
|
self.format_all(files, edit=0) |
|
sys.exit(0) # XXX Hack to suppress epilogue |
|
|
|
def last_changed(self, files): |
|
latest = 0 |
|
for file in files: |
|
entry = self.dir.open(file) |
|
if entry: |
|
mtime = mtime = entry.getmtime() |
|
if mtime > latest: |
|
latest = mtime |
|
print time.strftime(LAST_CHANGED, time.localtime(latest)) |
|
emit(EXPLAIN_MARKS) |
|
|
|
def format_all(self, files, edit=1, headers=1): |
|
sec = 0 |
|
for file in files: |
|
try: |
|
entry = self.dir.open(file) |
|
except NoSuchFile: |
|
continue |
|
if headers and entry.sec != sec: |
|
sec = entry.sec |
|
try: |
|
title = SECTION_TITLES[sec] |
|
except KeyError: |
|
title = "Untitled" |
|
emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n", |
|
sec=sec, title=title) |
|
entry.show(edit=edit) |
|
|
|
def do_index(self): |
|
self.prologue(T_INDEX) |
|
files = self.dir.list() |
|
self.last_changed(files) |
|
self.format_index(files, add=1) |
|
|
|
def format_index(self, files, add=0, localrefs=0): |
|
sec = 0 |
|
for file in files: |
|
try: |
|
entry = self.dir.open(file) |
|
except NoSuchFile: |
|
continue |
|
if entry.sec != sec: |
|
if sec: |
|
if add: |
|
emit(INDEX_ADDSECTION, sec=sec) |
|
emit(INDEX_ENDSECTION, sec=sec) |
|
sec = entry.sec |
|
try: |
|
title = SECTION_TITLES[sec] |
|
except KeyError: |
|
title = "Untitled" |
|
emit(INDEX_SECTION, sec=sec, title=title) |
|
if localrefs: |
|
emit(LOCAL_ENTRY, entry) |
|
else: |
|
emit(INDEX_ENTRY, entry) |
|
entry.emit_marks() |
|
if sec: |
|
if add: |
|
emit(INDEX_ADDSECTION, sec=sec) |
|
emit(INDEX_ENDSECTION, sec=sec) |
|
|
|
def do_recent(self): |
|
if not self.ui.days: |
|
days = 1 |
|
else: |
|
days = float(self.ui.days) |
|
try: |
|
cutoff = now - days * 24 * 3600 |
|
except OverflowError: |
|
cutoff = 0 |
|
list = [] |
|
for file in self.dir.list(): |
|
entry = self.dir.open(file) |
|
if not entry: |
|
continue |
|
mtime = entry.getmtime() |
|
if mtime >= cutoff: |
|
list.append((mtime, file)) |
|
list.sort() |
|
list.reverse() |
|
self.prologue(T_RECENT) |
|
if days <= 1: |
|
period = "%.2g hours" % (days*24) |
|
else: |
|
period = "%.6g days" % days |
|
if not list: |
|
emit(NO_RECENT, period=period) |
|
elif len(list) == 1: |
|
emit(ONE_RECENT, period=period) |
|
else: |
|
emit(SOME_RECENT, period=period, count=len(list)) |
|
self.format_all(map(lambda (mtime, file): file, list), headers=0) |
|
emit(TAIL_RECENT) |
|
|
|
def do_roulette(self): |
|
import random |
|
files = self.dir.list() |
|
if not files: |
|
self.error("No entries.") |
|
return |
|
file = random.choice(files) |
|
self.prologue(T_ROULETTE) |
|
emit(ROULETTE) |
|
self.dir.show(file) |
|
|
|
def do_help(self): |
|
self.prologue(T_HELP) |
|
emit(HELP) |
|
|
|
def do_show(self): |
|
entry = self.dir.open(self.ui.file) |
|
self.prologue(T_SHOW) |
|
entry.show() |
|
|
|
def do_add(self): |
|
self.prologue(T_ADD) |
|
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) |
|
emit(DELETE) |
|
|
|
def do_log(self): |
|
entry = self.dir.open(self.ui.file) |
|
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>') |
|
athead = 0 |
|
lines = output.split('\n') |
|
while lines and not lines[-1]: |
|
del lines[-1] |
|
if lines: |
|
line = lines[-1] |
|
if line[:1] == '=' and len(line) >= 40 and \ |
|
line == line[0]*len(line): |
|
del lines[-1] |
|
headrev = None |
|
for line in lines: |
|
if entry and athead and line[:9] == 'revision ': |
|
rev = line[9:].split() |
|
mami = revparse(rev) |
|
if not mami: |
|
print line |
|
else: |
|
emit(REVISIONLINK, entry, rev=rev, line=line) |
|
if mami[1] > 1: |
|
prev = "%d.%d" % (mami[0], mami[1]-1) |
|
emit(DIFFLINK, entry, prev=prev, rev=rev) |
|
if headrev: |
|
emit(DIFFLINK, entry, prev=rev, rev=headrev) |
|
else: |
|
headrev = rev |
|
print |
|
athead = 0 |
|
else: |
|
athead = 0 |
|
if line[:1] == '-' and len(line) >= 20 and \ |
|
line == len(line) * line[0]: |
|
athead = 1 |
|
sys.stdout.write('<HR>') |
|
else: |
|
print line |
|
print '</PRE>' |
|
|
|
def do_revision(self): |
|
entry = self.dir.open(self.ui.file) |
|
rev = self.ui.rev |
|
mami = revparse(rev) |
|
if not mami: |
|
self.error("Invalid revision number: %r." % (rev,)) |
|
self.prologue(T_REVISION, entry) |
|
self.shell(interpolate(SH_REVISION, entry, rev=rev)) |
|
|
|
def do_diff(self): |
|
entry = self.dir.open(self.ui.file) |
|
prev = self.ui.prev |
|
rev = self.ui.rev |
|
mami = revparse(rev) |
|
if not mami: |
|
self.error("Invalid revision number: %r." % (rev,)) |
|
if prev: |
|
if not revparse(prev): |
|
self.error("Invalid previous revision number: %r." % (prev,)) |
|
else: |
|
prev = '%d.%d' % (mami[0], mami[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>') |
|
print escape(output) |
|
print '</PRE>' |
|
|
|
def do_new(self): |
|
entry = self.dir.new(section=int(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(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): |
|
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 self.ui.title.split()[:1] != entry.title.split()[:1]: |
|
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("Commit version conflict!") |
|
emit(VERSIONCONFLICT, entry, self.ui) |
|
return |
|
commit_ok = ((not PASSWORD |
|
or self.ui.password == PASSWORD) |
|
and self.ui.author |
|
and '@' in self.ui.email |
|
and self.ui.log) |
|
if self.ui.commit: |
|
if not commit_ok: |
|
self.cantcommit() |
|
else: |
|
self.commit(entry) |
|
return |
|
self.prologue(T_REVIEW) |
|
emit(REVIEWHEAD) |
|
entry.body = self.ui.body |
|
entry.title = self.ui.title |
|
entry.show(edit=0) |
|
emit(EDITFORM1, self.ui, entry) |
|
if commit_ok: |
|
emit(COMMIT) |
|
else: |
|
emit(NOCOMMIT_HEAD) |
|
self.errordetail() |
|
emit(NOCOMMIT_TAIL) |
|
emit(EDITFORM2, self.ui, entry, load_my_cookie()) |
|
emit(EDITFORM3) |
|
|
|
def cantcommit(self): |
|
self.prologue(T_CANTCOMMIT) |
|
print CANTCOMMIT_HEAD |
|
self.errordetail() |
|
print CANTCOMMIT_TAIL |
|
|
|
def errordetail(self): |
|
if PASSWORD and self.ui.password != PASSWORD: |
|
emit(NEED_PASSWD) |
|
if not self.ui.log: |
|
emit(NEED_LOG) |
|
if not self.ui.author: |
|
emit(NEED_AUTHOR) |
|
if not self.ui.email: |
|
emit(NEED_EMAIL) |
|
|
|
def commit(self, entry): |
|
file = entry.file |
|
# Normalize line endings in body |
|
if '\r' in self.ui.body: |
|
self.ui.body = re.sub('\r\n?', '\n', self.ui.body) |
|
# Normalize whitespace in title |
|
self.ui.title = ' '.join(self.ui.title.split()) |
|
# Check that there were any changes |
|
if self.ui.body == entry.body and self.ui.title == entry.title: |
|
self.error("You didn't make any changes!") |
|
return |
|
|
|
# need to lock here because otherwise the file exists and is not writable (on NT) |
|
command = interpolate(SH_LOCK, file=file) |
|
p = os.popen(command) |
|
output = p.read() |
|
|
|
try: |
|
os.unlink(file) |
|
except os.error: |
|
pass |
|
try: |
|
f = open(file, 'w') |
|
except IOError, why: |
|
self.error(CANTWRITE, file=file, why=why) |
|
return |
|
date = time.ctime(now) |
|
emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0) |
|
f.write('\n') |
|
f.write(self.ui.body) |
|
f.write('\n') |
|
f.close() |
|
|
|
import tempfile |
|
tf = tempfile.NamedTemporaryFile() |
|
emit(LOGHEADER, self.ui, os.environ, date=date, _file=tf) |
|
tf.flush() |
|
tf.seek(0) |
|
|
|
command = interpolate(SH_CHECKIN, file=file, tfn=tf.name) |
|
log("\n\n" + command) |
|
p = os.popen(command) |
|
output = p.read() |
|
sts = p.close() |
|
log("output: " + output) |
|
log("done: " + str(sts)) |
|
log("TempFile:\n" + tf.read() + "end") |
|
|
|
if not sts: |
|
self.prologue(T_COMMITTED) |
|
emit(COMMITTED) |
|
else: |
|
self.error(T_COMMITFAILED) |
|
emit(COMMITFAILED, sts=sts) |
|
print '<PRE>%s</PRE>' % escape(output) |
|
|
|
try: |
|
os.unlink(tf.name) |
|
except os.error: |
|
pass |
|
|
|
entry = self.dir.open(file) |
|
entry.show() |
|
|
|
wiz = FaqWizard() |
|
wiz.go() |