85ddc719498a393b0f1eb537b441a257915d0fca
1 # $Id: template.py,v 1.5 2001-07-20 07:34:43 richard Exp $
3 import os, re, StringIO, urllib, cgi
5 import hyperdb, date
7 class Base:
8 def __init__(self, db, classname, nodeid=None, form=None):
9 self.db, self.classname, self.nodeid = db, classname, nodeid
10 self.form = form
11 self.cl = self.db.classes[self.classname]
12 self.properties = self.cl.getprops()
14 class Plain(Base):
15 ''' display a String property directly;
17 display a Date property in a specified time zone with an option to
18 omit the time from the date stamp;
20 for a Link or Multilink property, display the key strings of the
21 linked nodes (or the ids if the linked class has no key property)
22 '''
23 def __call__(self, property):
24 if not self.nodeid and self.form is None:
25 return '[Field: not called from item]'
26 propclass = self.properties[property]
27 if self.nodeid:
28 value = self.cl.get(self.nodeid, property)
29 else:
30 # TODO: pull the value from the form
31 if propclass.isMultilinkType: value = []
32 else: value = ''
33 if propclass.isStringType:
34 if value is None: value = ''
35 else: value = str(value)
36 elif propclass.isDateType:
37 value = str(value)
38 elif propclass.isIntervalType:
39 value = str(value)
40 elif propclass.isLinkType:
41 linkcl = self.db.classes[propclass.classname]
42 if value: value = str(linkcl.get(value, linkcl.getkey()))
43 else: value = '[unselected]'
44 elif propclass.isMultilinkType:
45 linkcl = self.db.classes[propclass.classname]
46 k = linkcl.getkey()
47 value = ', '.join([linkcl.get(i, k) for i in value])
48 else:
49 s = 'Plain: bad propclass "%s"'%propclass
50 return value
52 class Field(Base):
53 ''' display a property like the plain displayer, but in a text field
54 to be edited
55 '''
56 def __call__(self, property, size=None, height=None, showid=0):
57 if not self.nodeid and self.form is None:
58 return '[Field: not called from item]'
59 propclass = self.properties[property]
60 if self.nodeid:
61 value = self.cl.get(self.nodeid, property)
62 else:
63 # TODO: pull the value from the form
64 if propclass.isMultilinkType: value = []
65 else: value = ''
66 if (propclass.isStringType or propclass.isDateType or
67 propclass.isIntervalType):
68 size = size or 30
69 if value is None:
70 value = ''
71 else:
72 value = cgi.escape(value)
73 value = '"'.join(value.split('"'))
74 s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
75 elif propclass.isLinkType:
76 linkcl = self.db.classes[propclass.classname]
77 l = ['<select name="%s">'%property]
78 k = linkcl.getkey()
79 for optionid in linkcl.list():
80 option = linkcl.get(optionid, k)
81 s = ''
82 if optionid == value:
83 s = 'selected '
84 if showid:
85 lab = '%s%s: %s'%(propclass.classname, optionid, option)
86 else:
87 lab = option
88 if size is not None and len(lab) > size:
89 lab = lab[:size-3] + '...'
90 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
91 l.append('</select>')
92 s = '\n'.join(l)
93 elif propclass.isMultilinkType:
94 linkcl = self.db.classes[propclass.classname]
95 list = linkcl.list()
96 height = height or min(len(list), 7)
97 l = ['<select multiple name="%s" size="%s">'%(property, height)]
98 k = linkcl.getkey()
99 for optionid in list:
100 option = linkcl.get(optionid, k)
101 s = ''
102 if optionid in value:
103 s = 'selected '
104 if showid:
105 lab = '%s%s: %s'%(propclass.classname, optionid, option)
106 else:
107 lab = option
108 if size is not None and len(lab) > size:
109 lab = lab[:size-3] + '...'
110 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
111 l.append('</select>')
112 s = '\n'.join(l)
113 else:
114 s = 'Plain: bad propclass "%s"'%propclass
115 return s
117 class Menu(Base):
118 ''' for a Link property, display a menu of the available choices
119 '''
120 def __call__(self, property, size=None, height=None, showid=0):
121 propclass = self.properties[property]
122 if self.nodeid:
123 value = self.cl.get(self.nodeid, property)
124 else:
125 # TODO: pull the value from the form
126 if propclass.isMultilinkType: value = []
127 else: value = None
128 if propclass.isLinkType:
129 linkcl = self.db.classes[propclass.classname]
130 l = ['<select name="%s">'%property]
131 k = linkcl.getkey()
132 for optionid in linkcl.list():
133 option = linkcl.get(optionid, k)
134 s = ''
135 if optionid == value:
136 s = 'selected '
137 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
138 l.append('</select>')
139 return '\n'.join(l)
140 if propclass.isMultilinkType:
141 linkcl = self.db.classes[propclass.classname]
142 list = linkcl.list()
143 height = height or min(len(list), 7)
144 l = ['<select multiple name="%s" size="%s">'%(property, height)]
145 k = linkcl.getkey()
146 for optionid in list:
147 option = linkcl.get(optionid, k)
148 s = ''
149 if optionid in value:
150 s = 'selected '
151 if showid:
152 lab = '%s%s: %s'%(propclass.classname, optionid, option)
153 else:
154 lab = option
155 if size is not None and len(lab) > size:
156 lab = lab[:size-3] + '...'
157 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
158 l.append('</select>')
159 return '\n'.join(l)
160 return '[Menu: not a link]'
162 #XXX deviates from spec
163 class Link(Base):
164 ''' for a Link or Multilink property, display the names of the linked
165 nodes, hyperlinked to the item views on those nodes
166 for other properties, link to this node with the property as the text
167 '''
168 def __call__(self, property=None, **args):
169 if not self.nodeid and self.form is None:
170 return '[Link: not called from item]'
171 propclass = self.properties[property]
172 if self.nodeid:
173 value = self.cl.get(self.nodeid, property)
174 else:
175 if propclass.isMultilinkType: value = []
176 else: value = ''
177 if propclass.isLinkType:
178 linkcl = self.db.classes[propclass.classname]
179 linkvalue = linkcl.get(value, k)
180 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
181 if propclass.isMultilinkType:
182 linkcl = self.db.classes[propclass.classname]
183 l = []
184 for value in value:
185 linkvalue = linkcl.get(value, k)
186 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
187 return ', '.join(l)
188 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
190 class Count(Base):
191 ''' for a Multilink property, display a count of the number of links in
192 the list
193 '''
194 def __call__(self, property, **args):
195 if not self.nodeid:
196 return '[Count: not called from item]'
197 propclass = self.properties[property]
198 value = self.cl.get(self.nodeid, property)
199 if propclass.isMultilinkType:
200 return str(len(value))
201 return '[Count: not a Multilink]'
203 # XXX pretty is definitely new ;)
204 class Reldate(Base):
205 ''' display a Date property in terms of an interval relative to the
206 current date (e.g. "+ 3w", "- 2d").
208 with the 'pretty' flag, make it pretty
209 '''
210 def __call__(self, property, pretty=0):
211 if not self.nodeid and self.form is None:
212 return '[Reldate: not called from item]'
213 propclass = self.properties[property]
214 if not propclass.isDateType:
215 return '[Reldate: not a Date]'
216 if self.nodeid:
217 value = self.cl.get(self.nodeid, property)
218 else:
219 value = date.Date('.')
220 interval = value - date.Date('.')
221 if pretty:
222 if not self.nodeid:
223 return 'now'
224 pretty = interval.pretty()
225 if pretty is None:
226 pretty = value.pretty()
227 return pretty
228 return str(interval)
230 class Download(Base):
231 ''' show a Link("file") or Multilink("file") property using links that
232 allow you to download files
233 '''
234 def __call__(self, property, **args):
235 if not self.nodeid:
236 return '[Download: not called from item]'
237 propclass = self.properties[property]
238 value = self.cl.get(self.nodeid, property)
239 if propclass.isLinkType:
240 linkcl = self.db.classes[propclass.classname]
241 linkvalue = linkcl.get(value, k)
242 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
243 if propclass.isMultilinkType:
244 linkcl = self.db.classes[propclass.classname]
245 l = []
246 for value in value:
247 linkvalue = linkcl.get(value, k)
248 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
249 return ', '.join(l)
250 return '[Download: not a link]'
253 class Checklist(Base):
254 ''' for a Link or Multilink property, display checkboxes for the available
255 choices to permit filtering
256 '''
257 def __call__(self, property, **args):
258 propclass = self.properties[property]
259 if self.nodeid:
260 value = self.cl.get(self.nodeid, property)
261 else:
262 value = []
263 if propclass.isLinkType or propclass.isMultilinkType:
264 linkcl = self.db.classes[propclass.classname]
265 l = []
266 k = linkcl.getkey()
267 for optionid in linkcl.list():
268 option = linkcl.get(optionid, k)
269 if optionid in value:
270 checked = 'checked'
271 else:
272 checked = ''
273 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
274 option, checked, propclass.classname, option))
275 return '\n'.join(l)
276 return '[Checklist: not a link]'
278 class Note(Base):
279 ''' display a "note" field, which is a text area for entering a note to
280 go along with a change.
281 '''
282 def __call__(self, rows=5, cols=80):
283 # TODO: pull the value from the form
284 return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
285 cols)
287 # XXX new function
288 class List(Base):
289 ''' list the items specified by property using the standard index for
290 the class
291 '''
292 def __call__(self, property, **args):
293 propclass = self.properties[property]
294 if not propclass.isMultilinkType:
295 return '[List: not a Multilink]'
296 fp = StringIO.StringIO()
297 args['show_display_form'] = 0
298 value = self.cl.get(self.nodeid, property)
299 index(fp, self.db, propclass.classname, nodeids=value,
300 show_display_form=0)
301 return fp.getvalue()
303 # XXX new function
304 class History(Base):
305 ''' list the history of the item
306 '''
307 def __call__(self, **args):
308 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
309 '<tr class="list-header">',
310 '<td><span class="list-item"><strong>Date</strong></span></td>',
311 '<td><span class="list-item"><strong>User</strong></span></td>',
312 '<td><span class="list-item"><strong>Action</strong></span></td>',
313 '<td><span class="list-item"><strong>Args</strong></span></td>']
315 for id, date, user, action, args in self.cl.history(self.nodeid):
316 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
317 date, user, action, args))
318 l.append('</table>')
319 return '\n'.join(l)
321 # XXX new function
322 class Submit(Base):
323 ''' add a submit button for the item
324 '''
325 def __call__(self):
326 if self.nodeid:
327 return '<input type="submit" value="Submit Changes">'
328 elif self.form is not None:
329 return '<input type="submit" value="Submit New Entry">'
330 else:
331 return '[Submit: not called from item]'
334 #
335 # INDEX TEMPLATES
336 #
337 class IndexTemplateReplace:
338 def __init__(self, globals, locals, props):
339 self.globals = globals
340 self.locals = locals
341 self.props = props
343 def go(self, text, replace=re.compile(
344 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
345 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
346 return replace.sub(self, text)
348 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
349 if m.group('name'):
350 if m.group('name') in self.props:
351 text = m.group('text')
352 replace = IndexTemplateReplace(self.globals, {}, self.props)
353 return replace.go(m.group('text'))
354 else:
355 return ''
356 if m.group('display'):
357 command = m.group('command')
358 return eval(command, self.globals, self.locals)
359 print '*** unhandled match', m.groupdict()
361 def sortby(sort_name, columns, filter, sort, group, filterspec):
362 l = []
363 w = l.append
364 for k, v in filterspec.items():
365 k = urllib.quote(k)
366 if type(v) == type([]):
367 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
368 else:
369 w('%s=%s'%(k, urllib.quote(v)))
370 if columns:
371 w(':columns=%s'%','.join(map(urllib.quote, columns)))
372 if filter:
373 w(':filter=%s'%','.join(map(urllib.quote, filter)))
374 if group:
375 w(':group=%s'%','.join(map(urllib.quote, group)))
376 m = []
377 s_dir = ''
378 for name in sort:
379 dir = name[0]
380 if dir == '-':
381 dir = ''
382 else:
383 name = name[1:]
384 if sort_name == name:
385 if dir == '':
386 s_dir = '-'
387 elif dir == '-':
388 s_dir = ''
389 else:
390 m.append(dir+urllib.quote(name))
391 m.insert(0, s_dir+urllib.quote(sort_name))
392 # so things don't get completely out of hand, limit the sort to two columns
393 w(':sort=%s'%','.join(m[:2]))
394 return '&'.join(l)
396 def index(fp, db, classname, filterspec={}, filter=[], columns=[], sort=[],
397 group=[], show_display_form=1, nodeids=None,
398 col_re=re.compile(r'<property\s+name="([^>]+)">')):
400 globals = {
401 'plain': Plain(db, classname, form={}),
402 'field': Field(db, classname, form={}),
403 'menu': Menu(db, classname, form={}),
404 'link': Link(db, classname, form={}),
405 'count': Count(db, classname, form={}),
406 'reldate': Reldate(db, classname, form={}),
407 'download': Download(db, classname, form={}),
408 'checklist': Checklist(db, classname, form={}),
409 'list': List(db, classname, form={}),
410 'history': History(db, classname, form={}),
411 'submit': Submit(db, classname, form={}),
412 'note': Note(db, classname, form={})
413 }
414 cl = db.classes[classname]
415 properties = cl.getprops()
416 w = fp.write
418 try:
419 template = open(os.path.join('templates', classname+'.filter')).read()
420 all_filters = col_re.findall(template)
421 except IOError, error:
422 if error.errno != 2: raise
423 template = None
424 all_filters = []
425 if template and filter:
426 # display the filter section
427 w('<form>')
428 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
429 w('<tr class="location-bar">')
430 w(' <th align="left" colspan="2">Filter specification...</th>')
431 w('</tr>')
432 replace = IndexTemplateReplace(globals, locals(), filter)
433 w(replace.go(template))
434 if columns:
435 w('<input type="hidden" name=":columns" value="%s">'%','.join(columns))
436 if filter:
437 w('<input type="hidden" name=":filter" value="%s">'%','.join(filter))
438 if sort:
439 w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
440 if group:
441 w('<input type="hidden" name=":group" value="%s">'%','.join(group))
442 for k, v in filterspec.items():
443 if type(v) == type([]): v = ','.join(v)
444 w('<input type="hidden" name="%s" value="%s">'%(k, v))
445 w('<tr class="location-bar"><td width="1%%"> </td>')
446 w('<td><input type="submit" value="Redisplay"></td></tr>')
447 w('</table>')
448 w('</form>')
450 # XXX deviate from spec here ...
451 # load the index section template and figure the default columns from it
452 template = open(os.path.join('templates', classname+'.index')).read()
453 all_columns = col_re.findall(template)
454 if not columns:
455 columns = []
456 for name in all_columns:
457 columns.append(name)
458 else:
459 # re-sort columns to be the same order as all_columns
460 l = []
461 for name in all_columns:
462 if name in columns:
463 l.append(name)
464 columns = l
466 # now display the index section
467 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
468 w('<tr class="list-header">')
469 for name in columns:
470 cname = name.capitalize()
471 if show_display_form:
472 anchor = "%s?%s"%(classname, sortby(name, columns, filter,
473 sort, group, filterspec))
474 w('<td><span class="list-item"><a href="%s">%s</a></span></td>'%(
475 anchor, cname))
476 else:
477 w('<td><span class="list-item">%s</span></td>'%cname)
478 w('</tr>')
480 # this stuff is used for group headings - optimise the group names
481 old_group = None
482 group_names = []
483 if group:
484 for name in group:
485 if name[0] == '-': group_names.append(name[1:])
486 else: group_names.append(name)
488 # now actually loop through all the nodes we get from the filter and
489 # apply the template
490 if nodeids is None:
491 nodeids = cl.filter(filterspec, sort, group)
492 for nodeid in nodeids:
493 # check for a group heading
494 if group_names:
495 this_group = [cl.get(nodeid, name) for name in group_names]
496 if this_group != old_group:
497 l = []
498 for name in group_names:
499 prop = properties[name]
500 if prop.isLinkType:
501 group_cl = db.classes[prop.classname]
502 key = group_cl.getkey()
503 value = cl.get(nodeid, name)
504 if value is None:
505 l.append('[unselected %s]'%prop.classname)
506 else:
507 l.append(group_cl.get(cl.get(nodeid, name), key))
508 elif prop.isMultilinkType:
509 group_cl = db.classes[prop.classname]
510 key = group_cl.getkey()
511 for value in cl.get(nodeid, name):
512 l.append(group_cl.get(value, key))
513 else:
514 value = cl.get(nodeid, name)
515 if value is None:
516 value = '[empty %s]'%name
517 l.append(value)
518 w('<tr class="list-header">'
519 '<td align=left colspan=%s><strong>%s</strong></td></tr>'%(
520 len(columns), ', '.join(l)))
521 old_group = this_group
523 # display this node's row
524 for value in globals.values():
525 if hasattr(value, 'nodeid'):
526 value.nodeid = nodeid
527 replace = IndexTemplateReplace(globals, locals(), columns)
528 w(replace.go(template))
530 w('</table>')
532 if not show_display_form:
533 return
535 # now add in the filter/columns/group/etc config table form
536 w('<p><form>')
537 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
538 for k,v in filterspec.items():
539 if type(v) == type([]): v = ','.join(v)
540 w('<input type="hidden" name="%s" value="%s">'%(k, v))
541 if sort:
542 w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
543 names = []
544 for name in cl.getprops().keys():
545 if name in all_filters or name in all_columns:
546 names.append(name)
547 w('<tr class="location-bar">')
548 w('<th align="left" colspan=%s>View customisation...</th></tr>'%
549 (len(names)+1))
550 w('<tr class="location-bar"><th> </th>')
551 for name in names:
552 w('<th>%s</th>'%name.capitalize())
553 w('</tr>')
555 # filter
556 if all_filters:
557 w('<tr><th width="1%" align=right class="location-bar">Filters</th>')
558 for name in names:
559 if name not in all_filters:
560 w('<td> </td>')
561 continue
562 if name in filter: checked=' checked'
563 else: checked=''
564 w('<td align=middle>')
565 w('<input type="checkbox" name=":filter" value="%s" %s></td>'%(name,
566 checked))
567 w('</tr>')
569 # columns
570 if all_columns:
571 w('<tr><th width="1%" align=right class="location-bar">Columns</th>')
572 for name in names:
573 if name not in all_columns:
574 w('<td> </td>')
575 continue
576 if name in columns: checked=' checked'
577 else: checked=''
578 w('<td align=middle>')
579 w('<input type="checkbox" name=":columns" value="%s" %s></td>'%(
580 name, checked))
581 w('</tr>')
583 # group
584 w('<tr><th width="1%" align=right class="location-bar">Grouping</th>')
585 for name in names:
586 prop = properties[name]
587 if name not in all_columns:
588 w('<td> </td>')
589 continue
590 if name in group: checked=' checked'
591 else: checked=''
592 w('<td align=middle>')
593 w('<input type="checkbox" name=":group" value="%s" %s></td>'%(
594 name, checked))
595 w('</tr>')
597 w('<tr class="location-bar"><td width="1%"> </td>')
598 w('<td colspan="%s">'%len(names))
599 w('<input type="submit" value="Redisplay"></td></tr>')
600 w('</table>')
601 w('</form>')
604 #
605 # ITEM TEMPLATES
606 #
607 class ItemTemplateReplace:
608 def __init__(self, globals, locals, cl, nodeid):
609 self.globals = globals
610 self.locals = locals
611 self.cl = cl
612 self.nodeid = nodeid
614 def go(self, text, replace=re.compile(
615 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
616 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
617 return replace.sub(self, text)
619 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
620 if m.group('name'):
621 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
622 replace = ItemTemplateReplace(self.globals, {}, self.cl,
623 self.nodeid)
624 return replace.go(m.group('text'))
625 else:
626 return ''
627 if m.group('display'):
628 command = m.group('command')
629 return eval(command, self.globals, self.locals)
630 print '*** unhandled match', m.groupdict()
632 def item(fp, db, classname, nodeid, replace=re.compile(
633 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
634 r'(?P<endprop></property>)|'
635 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
637 globals = {
638 'plain': Plain(db, classname, nodeid),
639 'field': Field(db, classname, nodeid),
640 'menu': Menu(db, classname, nodeid),
641 'link': Link(db, classname, nodeid),
642 'count': Count(db, classname, nodeid),
643 'reldate': Reldate(db, classname, nodeid),
644 'download': Download(db, classname, nodeid),
645 'checklist': Checklist(db, classname, nodeid),
646 'list': List(db, classname, nodeid),
647 'history': History(db, classname, nodeid),
648 'submit': Submit(db, classname, nodeid),
649 'note': Note(db, classname, nodeid)
650 }
652 cl = db.classes[classname]
653 properties = cl.getprops()
655 if properties.has_key('type') and properties.has_key('content'):
656 pass
657 # XXX we really want to return this as a downloadable...
658 # currently I handle this at a higher level by detecting 'file'
659 # designators...
661 w = fp.write
662 w('<form action="%s%s">'%(classname, nodeid))
663 s = open(os.path.join('templates', classname+'.item')).read()
664 replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
665 w(replace.go(s))
666 w('</form>')
669 def newitem(fp, db, classname, form, replace=re.compile(
670 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
671 r'(?P<endprop></property>)|'
672 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
673 globals = {
674 'plain': Plain(db, classname, form=form),
675 'field': Field(db, classname, form=form),
676 'menu': Menu(db, classname, form=form),
677 'link': Link(db, classname, form=form),
678 'count': Count(db, classname, form=form),
679 'reldate': Reldate(db, classname, form=form),
680 'download': Download(db, classname, form=form),
681 'checklist': Checklist(db, classname, form=form),
682 'list': List(db, classname, form=form),
683 'history': History(db, classname, form=form),
684 'submit': Submit(db, classname, form=form),
685 'note': Note(db, classname, form=form)
686 }
688 cl = db.classes[classname]
689 properties = cl.getprops()
691 w = fp.write
692 try:
693 s = open(os.path.join('templates', classname+'.newitem')).read()
694 except:
695 s = open(os.path.join('templates', classname+'.item')).read()
696 w('<form action="new%s">'%classname)
697 replace = ItemTemplateReplace(globals, locals(), None, None)
698 w(replace.go(s))
699 w('</form>')
701 #
702 # $Log: not supported by cvs2svn $
703 # Revision 1.4 2001/07/19 06:27:07 anthonybaxter
704 # fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
705 # my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
706 # strings in a commit message. I'm a twonk.
707 #
708 # Also broke the help string in two.
709 #
710 # Revision 1.3 2001/07/19 05:52:22 anthonybaxter
711 # Added CVS keywords Id and Log to all python files.
712 #
713 #