faqwiz.py 24.4 KB
Newer Older
Guido van Rossum's avatar
Guido van Rossum committed
1 2 3 4 5 6 7 8 9
"""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.
10
The actual script to place in cgi-bin is faqw.py.
Guido van Rossum's avatar
Guido van Rossum committed
11

Guido van Rossum's avatar
Guido van Rossum committed
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()
Guido van Rossum's avatar
Guido van Rossum committed
17 18 19

class FileError:
    def __init__(self, file):
20
        self.file = file
Guido van Rossum's avatar
Guido van Rossum committed
21 22 23 24

class InvalidFile(FileError):
    pass

Guido van Rossum's avatar
Guido van Rossum committed
25 26
class NoSuchSection(FileError):
    def __init__(self, section):
27 28
        FileError.__init__(self, NEWFILENAME %(section, 1))
        self.section = section
Guido van Rossum's avatar
Guido van Rossum committed
29

Guido van Rossum's avatar
Guido van Rossum committed
30 31
class NoSuchFile(FileError):
    def __init__(self, file, why=None):
32 33
        FileError.__init__(self, file)
        self.why = why
Guido van Rossum's avatar
Guido van Rossum committed
34

Guido van Rossum's avatar
Guido van Rossum committed
35
def escape(s):
36 37 38
    s = string.replace(s, '&', '&')
    s = string.replace(s, '<', '&lt;')
    s = string.replace(s, '>', '&gt;')
Guido van Rossum's avatar
Guido van Rossum committed
39 40
    return s

Guido van Rossum's avatar
Guido van Rossum committed
41 42
def escapeq(s):
    s = escape(s)
43
    s = string.replace(s, '"', '&quot;')
Guido van Rossum's avatar
Guido van Rossum committed
44 45
    return s

Guido van Rossum's avatar
Guido van Rossum committed
46 47
def _interpolate(format, args, kw):
    try:
48
        quote = kw['_quote']
Guido van Rossum's avatar
Guido van Rossum committed
49
    except KeyError:
50
        quote = 1
Guido van Rossum's avatar
Guido van Rossum committed
51 52 53 54 55 56
    d = (kw,) + args + (faqconf.__dict__,)
    m = MagicDict(d, quote)
    return format % m

def interpolate(format, *args, **kw):
    return _interpolate(format, args, kw)
Guido van Rossum's avatar
Guido van Rossum committed
57

Guido van Rossum's avatar
Guido van Rossum committed
58 59
def emit(format, *args, **kw):
    try:
60
        f = kw['_file']
Guido van Rossum's avatar
Guido van Rossum committed
61
    except KeyError:
62
        f = sys.stdout
Guido van Rossum's avatar
Guido van Rossum committed
63
    f.write(_interpolate(format, args, kw))
Guido van Rossum's avatar
Guido van Rossum committed
64 65 66

translate_prog = None

67
def translate(text, pre=0):
Guido van Rossum's avatar
Guido van Rossum committed
68 69
    global translate_prog
    if not translate_prog:
70 71
        translate_prog = prog = re.compile(
            r'\b(http|ftp|https)://\S+(\b|/)|\b[-.\w]+@[-.\w]+')
Guido van Rossum's avatar
Guido van Rossum committed
72
    else:
73
        prog = translate_prog
Guido van Rossum's avatar
Guido van Rossum committed
74 75 76
    i = 0
    list = []
    while 1:
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
        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:
92
                repl = '<A HREF="mailto:%s">%s</A>' % (url, url)
93 94 95
        else:
            repl = url
        list.append(repl)
Guido van Rossum's avatar
Guido van Rossum committed
96
    j = len(text)
Guido van Rossum's avatar
Guido van Rossum committed
97
    list.append(escape(text[i:j]))
Guido van Rossum's avatar
Guido van Rossum committed
98 99 100
    return string.join(list, '')

def emphasize(line):
101
    return re.sub(r'\*([a-zA-Z]+)\*', r'<I>\1</I>', line)
Guido van Rossum's avatar
Guido van Rossum committed
102

103 104 105 106 107
revparse_prog = None

def revparse(rev):
    global revparse_prog
    if not revparse_prog:
108
        revparse_prog = re.compile(r'^(\d{1,3})\.(\d{1,4})$')
109 110
    m = revparse_prog.match(rev)
    if not m:
111
        return None
112
    [major, minor] = map(string.atoi, m.group(1, 2))
113 114
    return major, minor

115 116 117 118 119 120 121
logon = 0
def log(text):
    if logon:
        logfile = open("logfile", "a")
        logfile.write(text + "\n")
        logfile.close()

Guido van Rossum's avatar
Guido van Rossum committed
122 123
def load_cookies():
    if not os.environ.has_key('HTTP_COOKIE'):
124
        return {}
Guido van Rossum's avatar
Guido van Rossum committed
125 126 127 128
    raw = os.environ['HTTP_COOKIE']
    words = map(string.strip, string.split(raw, ';'))
    cookies = {}
    for word in words:
129 130 131 132
        i = string.find(word, '=')
        if i >= 0:
            key, value = word[:i], word[i+1:]
            cookies[key] = value
Guido van Rossum's avatar
Guido van Rossum committed
133 134 135 136 137
    return cookies

def load_my_cookie():
    cookies = load_cookies()
    try:
138
        value = cookies[COOKIE_NAME]
Guido van Rossum's avatar
Guido van Rossum committed
139
    except KeyError:
140
        return {}
Guido van Rossum's avatar
Guido van Rossum committed
141 142 143 144
    import urllib
    value = urllib.unquote(value)
    words = string.split(value, '/')
    while len(words) < 3:
145
        words.append('')
Guido van Rossum's avatar
Guido van Rossum committed
146 147 148 149
    author = string.join(words[:-2], '/')
    email = words[-2]
    password = words[-1]
    return {'author': author,
150 151
            'email': email,
            'password': password}
Guido van Rossum's avatar
Guido van Rossum committed
152

Guido van Rossum's avatar
Guido van Rossum committed
153 154 155 156 157 158 159
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)
160 161
    path = os.environ.get('SCRIPT_NAME', '/cgi-bin/')
    print "Set-Cookie: %s=%s; path=%s;" % (name, value, path),
162
    print time.strftime("expires=%a, %d-%b-%y %X GMT", gmt)
Guido van Rossum's avatar
Guido van Rossum committed
163 164

class MagicDict:
Guido van Rossum's avatar
Guido van Rossum committed
165

Guido van Rossum's avatar
Guido van Rossum committed
166
    def __init__(self, d, quote):
167 168
        self.__d = d
        self.__quote = quote
Guido van Rossum's avatar
Guido van Rossum committed
169 170

    def __getitem__(self, key):
171 172 173 174 175 176 177 178 179 180 181
        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 ''
Guido van Rossum's avatar
Guido van Rossum committed
182 183 184 185

class UserInput:

    def __init__(self):
186
        self.__form = cgi.FieldStorage()
187
        #log("\n\nbody: " + self.body)
Guido van Rossum's avatar
Guido van Rossum committed
188 189

    def __getattr__(self, name):
190 191 192 193 194 195 196 197 198 199
        if name[0] == '_':
            raise AttributeError
        try:
            value = self.__form[name].value
        except (TypeError, KeyError):
            value = ''
        else:
            value = string.strip(value)
        setattr(self, name, value)
        return value
Guido van Rossum's avatar
Guido van Rossum committed
200 201

    def __getitem__(self, key):
202
        return getattr(self, key)
Guido van Rossum's avatar
Guido van Rossum committed
203

Guido van Rossum's avatar
Guido van Rossum committed
204 205 206
class FaqEntry:

    def __init__(self, fp, file, sec_num):
207 208 209 210 211 212 213 214 215
        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 = ''
Guido van Rossum's avatar
Guido van Rossum committed
216 217

    def __getattr__(self, name):
218 219 220 221 222 223 224 225 226
        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
Guido van Rossum's avatar
Guido van Rossum committed
227

Guido van Rossum's avatar
Guido van Rossum committed
228
    def __getitem__(self, key):
229
        return getattr(self, key)
Guido van Rossum's avatar
Guido van Rossum committed
230 231

    def load_version(self):
232 233 234 235 236 237 238 239 240 241 242
        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
Guido van Rossum's avatar
Guido van Rossum committed
243

244
    def getmtime(self):
245 246 247 248 249 250
        if not self.last_changed_date:
            return 0
        try:
            return os.stat(self.file)[stat.ST_MTIME]
        except os.error:
            return 0
251 252

    def emit_marks(self):
253 254 255 256 257
        mtime = self.getmtime()
        if mtime >= now - DT_VERY_RECENT:
            emit(MARK_VERY_RECENT, self)
        elif mtime >= now - DT_RECENT:
            emit(MARK_RECENT, self)
258

Guido van Rossum's avatar
Guido van Rossum committed
259
    def show(self, edit=1):
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
        emit(ENTRY_HEADER1, self)
        self.emit_marks()
        emit(ENTRY_HEADER2, self)
        pre = 0
        raw = 0
        for line in string.split(self.body, '\n'):
            # Allow the user to insert raw html into a FAQ answer
            # (Skip Montanaro, with changes by Guido)
            tag = string.lower(string.rstrip(line))
            if tag == '<html>':
                raw = 1
                continue
            if tag == '</html>':
                raw = 0
                continue
            if raw:
                print line
                continue
            if not string.strip(line):
                if pre:
                    print '</PRE>'
                    pre = 0
                else:
                    print '<P>'
            else:
                if line[0] not in string.whitespace:
                    if pre:
                        print '</PRE>'
                        pre = 0
                else:
                    if not pre:
                        print '<PRE>'
                        pre = 1
                if '/' in line or '@' in line:
                    line = 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>'
Guido van Rossum's avatar
Guido van Rossum committed
309 310 311 312 313

class FaqDir:

    entryclass = FaqEntry

314
    __okprog = re.compile(OKFILENAME)
Guido van Rossum's avatar
Guido van Rossum committed
315 316

    def __init__(self, dir=os.curdir):
317 318
        self.__dir = dir
        self.__files = None
Guido van Rossum's avatar
Guido van Rossum committed
319 320

    def __fill(self):
321 322 323 324 325 326 327 328
        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()
Guido van Rossum's avatar
Guido van Rossum committed
329 330

    def good(self, file):
331
        return self.__okprog.match(file)
Guido van Rossum's avatar
Guido van Rossum committed
332 333

    def parse(self, file):
334 335 336 337 338
        m = self.good(file)
        if not m:
            return None
        sec, num = m.group(1, 2)
        return string.atoi(sec), string.atoi(num)
Guido van Rossum's avatar
Guido van Rossum committed
339 340

    def list(self):
341 342 343
        # XXX Caller shouldn't modify result
        self.__fill()
        return self.__files
Guido van Rossum's avatar
Guido van Rossum committed
344 345

    def open(self, file):
346 347 348 349 350 351 352 353 354 355 356
        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()
Guido van Rossum's avatar
Guido van Rossum committed
357 358

    def show(self, file, edit=1):
359
        self.open(file).show(edit=edit)
Guido van Rossum's avatar
Guido van Rossum committed
360

Guido van Rossum's avatar
Guido van Rossum committed
361
    def new(self, section):
362 363 364 365 366 367 368 369 370 371
        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)
Guido van Rossum's avatar
Guido van Rossum committed
372 373 374 375

class FaqWizard:

    def __init__(self):
376 377
        self.ui = UserInput()
        self.dir = FaqDir()
Guido van Rossum's avatar
Guido van Rossum committed
378 379

    def go(self):
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
        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 %s." % `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()
Guido van Rossum's avatar
Guido van Rossum committed
397 398

    def error(self, message, **kw):
399 400
        self.prologue(T_ERROR)
        emit(message, kw)
Guido van Rossum's avatar
Guido van Rossum committed
401 402

    def prologue(self, title, entry=None, **kw):
403
        emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
Guido van Rossum's avatar
Guido van Rossum committed
404 405

    def epilogue(self):
406
        emit(EPILOGUE)
Guido van Rossum's avatar
Guido van Rossum committed
407 408

    def do_home(self):
409 410
        self.prologue(T_HOME)
        emit(HOME)
Guido van Rossum's avatar
Guido van Rossum committed
411 412

    def do_debug(self):
413 414 415
        self.prologue("FAQ Wizard Debugging")
        form = cgi.FieldStorage()
        cgi.print_form(form)
Guido van Rossum's avatar
Guido van Rossum committed
416
        cgi.print_environ(os.environ)
417 418
        cgi.print_directory()
        cgi.print_arguments()
Guido van Rossum's avatar
Guido van Rossum committed
419 420

    def do_search(self):
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
        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 = [string.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)
Guido van Rossum's avatar
Guido van Rossum committed
472 473

    def do_all(self):
474 475 476 477 478
        self.prologue(T_ALL)
        files = self.dir.list()
        self.last_changed(files)
        self.format_index(files, localrefs=1)
        self.format_all(files)
Guido van Rossum's avatar
Guido van Rossum committed
479 480

    def do_compat(self):
481 482 483 484 485 486
        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
Guido van Rossum's avatar
Guido van Rossum committed
487 488

    def last_changed(self, files):
489 490 491 492 493 494 495 496 497
        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)
Guido van Rossum's avatar
Guido van Rossum committed
498

499
    def format_all(self, files, edit=1, headers=1):
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
        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)
Guido van Rossum's avatar
Guido van Rossum committed
515 516

    def do_index(self):
517 518 519 520
        self.prologue(T_INDEX)
        files = self.dir.list()
        self.last_changed(files)
        self.format_index(files, add=1)
Guido van Rossum's avatar
Guido van Rossum committed
521

522
    def format_index(self, files, add=0, localrefs=0):
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
        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)
Guido van Rossum's avatar
Guido van Rossum committed
549 550

    def do_recent(self):
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
        if not self.ui.days:
            days = 1
        else:
            days = string.atof(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)
Guido van Rossum's avatar
Guido van Rossum committed
582 583

    def do_roulette(self):
Guido van Rossum's avatar
Guido van Rossum committed
584
        import random
585 586 587 588
        files = self.dir.list()
        if not files: 
            self.error("No entries.")
            return
Guido van Rossum's avatar
Guido van Rossum committed
589
        file = random.choice(files)
590 591 592
        self.prologue(T_ROULETTE)
        emit(ROULETTE)
        self.dir.show(file)
Guido van Rossum's avatar
Guido van Rossum committed
593 594

    def do_help(self):
595 596
        self.prologue(T_HELP)
        emit(HELP)
Guido van Rossum's avatar
Guido van Rossum committed
597 598

    def do_show(self):
599 600 601
        entry = self.dir.open(self.ui.file)
        self.prologue(T_SHOW)
        entry.show()
Guido van Rossum's avatar
Guido van Rossum committed
602 603

    def do_add(self):
604 605 606 607 608 609 610
        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)
Guido van Rossum's avatar
Guido van Rossum committed
611 612

    def do_delete(self):
613 614
        self.prologue(T_DELETE)
        emit(DELETE)
Guido van Rossum's avatar
Guido van Rossum committed
615 616

    def do_log(self):
617 618 619 620
        entry = self.dir.open(self.ui.file)
        self.prologue(T_LOG, entry)
        emit(LOG, entry)
        self.rlog(interpolate(SH_RLOG, entry), entry)
Guido van Rossum's avatar
Guido van Rossum committed
621 622

    def rlog(self, command, entry=None):
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
        output = os.popen(command).read()
        sys.stdout.write('<PRE>')
        athead = 0
        lines = string.split(output, '\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 = string.strip(line[9:])
                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>'
Guido van Rossum's avatar
Guido van Rossum committed
661

662
    def do_revision(self):
663 664 665 666 667 668 669
        entry = self.dir.open(self.ui.file)
        rev = self.ui.rev
        mami = revparse(rev)
        if not mami:
            self.error("Invalid revision number: %s." % `rev`)
        self.prologue(T_REVISION, entry)
        self.shell(interpolate(SH_REVISION, entry, rev=rev))
670

Guido van Rossum's avatar
Guido van Rossum committed
671
    def do_diff(self):
672 673 674 675 676 677 678 679 680 681 682 683 684
        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: %s." % `rev`)
        if prev:
            if not revparse(prev):
                self.error("Invalid previous revision number: %s." % `prev`)
        else:
            prev = '%d.%d' % (mami[0], mami[1])
        self.prologue(T_DIFF, entry)
        self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
Guido van Rossum's avatar
Guido van Rossum committed
685 686

    def shell(self, command):
687 688 689 690
        output = os.popen(command).read()
        sys.stdout.write('<PRE>')
        print escape(output)
        print '</PRE>'
Guido van Rossum's avatar
Guido van Rossum committed
691 692

    def do_new(self):
693 694 695 696 697 698 699 700
        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)
Guido van Rossum's avatar
Guido van Rossum committed
701 702

    def do_edit(self):
703 704 705 706 707 708 709 710
        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)
Guido van Rossum's avatar
Guido van Rossum committed
711 712

    def do_review(self):
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
        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 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:
754 755 756
            emit(NOCOMMIT_HEAD)
            self.errordetail()
            emit(NOCOMMIT_TAIL)
757 758
        emit(EDITFORM2, self.ui, entry, load_my_cookie())
        emit(EDITFORM3)
Guido van Rossum's avatar
Guido van Rossum committed
759 760

    def cantcommit(self):
761 762
        self.prologue(T_CANTCOMMIT)
        print CANTCOMMIT_HEAD
763 764 765 766 767
        self.errordetail()
        print CANTCOMMIT_TAIL

    def errordetail(self):
        if PASSWORD and self.ui.password != PASSWORD:
768 769 770 771 772 773 774
            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)
Guido van Rossum's avatar
Guido van Rossum committed
775 776

    def commit(self, entry):
777 778 779 780 781 782 783 784 785 786
        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 = 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("You didn't make any changes!")
            return
787 788 789 790 791 792

        # 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()

793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814
        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
        tfn = tempfile.mktemp()
        f = open(tfn, 'w')
        emit(LOGHEADER, self.ui, os.environ, date=date, _file=f)
        f.close()

815 816
        command = interpolate(SH_CHECKIN, file=file, tfn=tfn)
        log("\n\n" + command)
817 818 819
        p = os.popen(command)
        output = p.read()
        sts = p.close()
820 821 822 823
        log("output: " + output)
        log("done: " + str(sts))
        log("TempFile:\n" + open(tfn).read() + "end")
        
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
        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(tfn)
        except os.error:
            pass

        entry = self.dir.open(file)
        entry.show()
Guido van Rossum's avatar
Guido van Rossum committed
839 840 841

wiz = FaqWizard()
wiz.go()