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