1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 # $Id: htmltemplate.py,v 1.111 2002-08-15 00:40:10 richard Exp $
20 __doc__ = """
21 Template engine.
23 Three types of template files exist:
24 .index used by IndexTemplate
25 .item used by ItemTemplate and NewItemTemplate
26 .filter used by IndexTemplate
28 Templating works by instantiating one of the *Template classes above,
29 passing in a handle to the cgi client, identifying the class and the
30 template source directory.
32 The *Template class reads in the parsed template (parsing and caching
33 as needed). When the render() method is called, the parse tree is
34 traversed. Each node is either text (immediately output), a Require
35 instance (resulting in a call to _test()), a Property instance (treated
36 differently by .item and .index) or a Diplay instance (resulting in
37 a call to one of the template_funcs.py functions).
39 In a .index list, Property tags are used to determine columns, and
40 disappear before the actual rendering. Note that the template will
41 be rendered many times in a .index.
43 In a .item, Property tags check if the node has the property.
45 Templating is tested by the test_htmltemplate unit test suite. If you add
46 a template function, add a test for all data types or the angry pink bunny
47 will hunt you down.
48 """
49 import weakref, os, types, cgi, sys, urllib, re
50 try:
51 import cPickle as pickle
52 except ImportError:
53 import pickle
54 from template_parser import RoundupTemplate, Display, Property, Require
55 from i18n import _
56 import hyperdb, template_funcs
58 MTIME = os.path.stat.ST_MTIME
60 class MissingTemplateError(ValueError):
61 '''Error raised when a template file is missing
62 '''
63 pass
65 # what a <require> tag results in
66 def _test(attributes, client, classname, nodeid):
67 tests = {}
68 for nm, val in attributes:
69 tests[nm] = val
70 userid = client.db.user.lookup(client.user)
71 security = client.db.security
72 perms = tests.get('permission', None)
73 if perms:
74 del tests['permission']
75 perms = perms.split(',')
76 for value in perms:
77 if security.hasPermission(value, userid, classname):
78 # just passing the permission is OK
79 return 1
80 # try the attr conditions until one is met
81 if nodeid is None:
82 return 0
83 if not tests:
84 return 0
85 for propname, value in tests.items():
86 if value == '$userid':
87 tests[propname] = userid
88 return security.hasNodePermission(classname, nodeid, **tests)
90 # what a <display> tag results in
91 def _display(attributes, client, classname, cl, props, nodeid, filterspec=None):
92 call = attributes[0][1] #eg "field('prop2')"
93 pos = call.find('(')
94 funcnm = call[:pos]
95 func = templatefuncs.get(funcnm, None)
96 if func:
97 argstr = call[pos:]
98 args, kws = eval('splitargs'+argstr)
99 args = (client, classname, cl, props, nodeid, filterspec) + args
100 rslt = func(*args, **kws)
101 else:
102 rslt = _('no template function %s' % funcnm)
103 client.write(rslt)
105 # what a <property> tag results in
106 def _exists(attributes, cl, props, nodeid):
107 nm = attributes[0][1]
108 if nodeid:
109 return cl.get(nodeid, nm)
110 return props.get(nm, 0)
112 class Template:
113 ''' base class of all templates.
115 knows how to compile & load a template.
116 knows how to render one item. '''
117 def __init__(self, client, templates, classname):
118 if isinstance(client, weakref.ProxyType):
119 self.client = client
120 else:
121 self.client = weakref.proxy(client)
122 self.templatedir = templates
123 self.compiledtemplatedir = self.templatedir + 'c'
124 self.classname = classname
125 self.cl = self.client.db.getclass(self.classname)
126 self.properties = self.cl.getprops()
127 self.template = self._load()
128 self.filterspec = None
129 self.columns = None
130 self.nodeid = None
132 def _load(self):
133 ''' Load a template from disk and parse it.
135 Once parsed, the template is stored as a pickle in the
136 "htmlc" directory of the instance. If the file in there is
137 newer than the source template file, it's used in preference so
138 we don't have to re-parse.
139 '''
140 # figure where the template source is
141 src = os.path.join(self.templatedir, self.classname + self.extension)
143 if not os.path.exists(src):
144 # hrm, nothing exactly matching what we're after, see if we can
145 # fall back on another template
146 if hasattr(self, 'fallbackextension'):
147 self.extension = self.fallbackextension
148 return self._load()
149 raise MissingTemplateError, self.classname + self.extension
151 # figure where the compiled template should be
152 cpl = os.path.join(self.compiledtemplatedir,
153 self.classname + self.extension)
155 if (not os.path.exists(cpl)
156 or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
157 # there's either no compiled template, or it's out of date
158 parser = RoundupTemplate()
159 parser.feed(open(src, 'r').read())
160 tmplt = parser.structure
161 try:
162 if not os.path.exists(self.compiledtemplatedir):
163 os.makedirs(self.compiledtemplatedir)
164 f = open(cpl, 'wb')
165 pickle.dump(tmplt, f)
166 f.close()
167 except Exception, e:
168 print "ouch in pickling: got a %s %r" % (e, e.args)
169 pass
170 else:
171 # load the compiled template
172 f = open(cpl, 'rb')
173 tmplt = pickle.load(f)
174 return tmplt
176 def _render(self, tmplt=None, test=_test, display=_display, exists=_exists):
177 ''' Render the template
178 '''
179 if tmplt is None:
180 tmplt = self.template
182 # go through the list of template "commands"
183 for entry in tmplt:
184 if isinstance(entry, type('')):
185 # string - just write it out
186 self.client.write(entry)
188 elif isinstance(entry, Require):
189 # a <require> tag
190 if test(entry.attributes, self.client, self.classname,
191 self.nodeid):
192 # require test passed, render the ok clause
193 self._render(entry.ok)
194 elif entry.fail:
195 # if there's a fail clause, render it
196 self._render(entry.fail)
198 elif isinstance(entry, Display):
199 # execute the <display> function
200 display(entry.attributes, self.client, self.classname,
201 self.cl, self.properties, self.nodeid, self.filterspec)
203 elif isinstance(entry, Property):
204 # do a <property> test
205 if self.columns is None:
206 # doing an Item - see if the property is present
207 if exists(entry.attributes, self.cl, self.properties,
208 self.nodeid):
209 self._render(entry.ok)
210 # XXX erm, should this be commented out?
211 #elif entry.attributes[0][1] in self.columns:
212 else:
213 self._render(entry.ok)
215 class IndexTemplate(Template):
216 ''' renders lists of items
218 shows filter form (for new queries / to refine queries)
219 has clickable column headers (sort by this column / sort reversed)
220 has group by lines
221 has full text search match lines '''
222 extension = '.index'
224 def __init__(self, client, templates, classname):
225 Template.__init__(self, client, templates, classname)
227 def render(self, **kw):
228 ''' Render the template - well, wrap the rendering in a try/finally
229 so we're guaranteed to clean up after ourselves
230 '''
231 try:
232 self.renderInner(**kw)
233 finally:
234 self.cl = self.properties = self.client = None
236 def renderInner(self, filterspec={}, search_text='', filter=[], columns=[],
237 sort=[], group=[], show_display_form=1, nodeids=None,
238 show_customization=1, show_nodes=1, pagesize=50, startwith=0,
239 simple_search=1, xtracols=None):
240 ''' Take all the index arguments and render some HTML
241 '''
243 self.filterspec = filterspec
244 w = self.client.write
245 cl = self.cl
246 properties = self.properties
247 if xtracols is None:
248 xtracols = []
250 # XXX deviate from spec here ...
251 # load the index section template and figure the default columns from it
252 displayable_props = []
253 all_columns = []
254 for node in self.template:
255 if isinstance(node, Property):
256 colnm = node.attributes[0][1]
257 if properties.has_key(colnm):
258 displayable_props.append(colnm)
259 all_columns.append(colnm)
260 elif colnm in xtracols:
261 all_columns.append(colnm)
262 if not columns:
263 columns = all_columns
264 else:
265 # re-sort columns to be the same order as displayable_props
266 l = []
267 for name in all_columns:
268 if name in columns:
269 l.append(name)
270 columns = l
271 self.columns = columns
273 # optimize the template
274 self.template = self._optimize(self.template)
276 # display the filter section
277 if (show_display_form and
278 self.client.instance.FILTER_POSITION.startswith('top')):
279 w('<form onSubmit="return submit_once()" action="%s">\n'%
280 self.client.classname)
281 self.filter_section(search_text, filter, columns, group,
282 displayable_props, sort, filterspec, pagesize, startwith,
283 simple_search)
285 # now display the index section
286 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
287 w('<tr class="list-header">\n')
288 for name in columns:
289 cname = name.capitalize()
290 if show_display_form and not cname in xtracols:
291 sb = self.sortby(name, search_text, filterspec, columns, filter,
292 group, sort, pagesize)
293 anchor = "%s?%s"%(self.client.classname, sb)
294 w('<td><span class="list-header"><a href="%s">%s</a>'
295 '</span></td>\n'%(anchor, cname))
296 else:
297 w('<td><span class="list-header">%s</span></td>\n'%cname)
298 w('</tr>\n')
300 # this stuff is used for group headings - optimise the group names
301 old_group = None
302 group_names = []
303 if group:
304 for name in group:
305 if name[0] == '-': group_names.append(name[1:])
306 else: group_names.append(name)
308 # now actually loop through all the nodes we get from the filter and
309 # apply the template
310 if show_nodes:
311 matches = None
312 if nodeids is None:
313 if search_text != '':
314 matches = self.client.db.indexer.search(
315 re.findall(r'\b\w{2,25}\b', search_text), cl)
316 nodeids = cl.filter(matches, filterspec, sort, group)
317 linecount = 0
318 for nodeid in nodeids[startwith:startwith+pagesize]:
319 # check for a group heading
320 if group_names:
321 this_group = [cl.get(nodeid, name, _('[no value]'))
322 for name in group_names]
323 if this_group != old_group:
324 l = []
325 for name in group_names:
326 prop = properties[name]
327 if isinstance(prop, hyperdb.Link):
328 group_cl = self.client.db.getclass(prop.classname)
329 key = group_cl.getkey()
330 if key is None:
331 key = group_cl.labelprop()
332 value = cl.get(nodeid, name)
333 if value is None:
334 l.append(_('[unselected %(classname)s]')%{
335 'classname': prop.classname})
336 else:
337 l.append(group_cl.get(value, key))
338 elif isinstance(prop, hyperdb.Multilink):
339 group_cl = self.client.db.getclass(prop.classname)
340 key = group_cl.getkey()
341 for value in cl.get(nodeid, name):
342 l.append(group_cl.get(value, key))
343 else:
344 value = cl.get(nodeid, name,
345 _('[no value]'))
346 if value is None:
347 value = _('[empty %(name)s]')%locals()
348 else:
349 value = str(value)
350 l.append(value)
351 w('<tr class="section-bar">'
352 '<td align=middle colspan=%s>'
353 '<strong>%s</strong></td></tr>\n'%(
354 len(columns), ', '.join(l)))
355 old_group = this_group
357 # display this node's row
358 self.nodeid = nodeid
359 self._render()
360 if matches:
361 self.node_matches(matches[nodeid], len(columns))
362 self.nodeid = None
364 w('</table>\n')
365 # the previous and next links
366 if nodeids:
367 baseurl = self.buildurl(filterspec, search_text, filter,
368 columns, sort, group, pagesize)
369 if startwith > 0:
370 prevurl = '<a href="%s&:startwith=%s"><< '\
371 'Previous page</a>'%(baseurl, max(0, startwith-pagesize))
372 else:
373 prevurl = ""
374 if startwith + pagesize < len(nodeids):
375 nexturl = '<a href="%s&:startwith=%s">Next page '\
376 '>></a>'%(baseurl, startwith+pagesize)
377 else:
378 nexturl = ""
379 if prevurl or nexturl:
380 w('''<table width="100%%"><tr>
381 <td width="50%%" align="center">%s</td>
382 <td width="50%%" align="center">%s</td>
383 </tr></table>\n'''%(prevurl, nexturl))
385 # display the filter section
386 if (show_display_form and hasattr(self.client.instance,
387 'FILTER_POSITION') and
388 self.client.instance.FILTER_POSITION.endswith('bottom')):
389 w('<form onSubmit="return submit_once()" action="%s">\n'%
390 self.client.classname)
391 self.filter_section(search_text, filter, columns, group,
392 displayable_props, sort, filterspec, pagesize, startwith,
393 simple_search)
395 def _optimize(self, tmplt):
396 columns = self.columns
397 t = []
398 for entry in tmplt:
399 if isinstance(entry, Property):
400 if entry.attributes[0][1] in columns:
401 t.extend(entry.ok)
402 else:
403 t.append(entry)
404 return t
406 def buildurl(self, filterspec, search_text, filter, columns, sort, group,
407 pagesize):
408 d = {'pagesize':pagesize, 'pagesize':pagesize,
409 'classname':self.classname}
410 if search_text:
411 d['searchtext'] = 'search_text=%s&' % search_text
412 else:
413 d['searchtext'] = ''
414 d['filter'] = ','.join(map(urllib.quote,filter))
415 d['columns'] = ','.join(map(urllib.quote,columns))
416 d['sort'] = ','.join(map(urllib.quote,sort))
417 d['group'] = ','.join(map(urllib.quote,group))
418 tmp = []
419 for col, vals in filterspec.items():
420 vals = ','.join(map(urllib.quote,vals))
421 tmp.append('%s=%s' % (col, vals))
422 d['filters'] = '&'.join(tmp)
423 return ('%(classname)s?%(searchtext)s%(filters)s&:sort=%(sort)s&'
424 ':filter=%(filter)s&:group=%(group)s&:columns=%(columns)s&'
425 ':pagesize=%(pagesize)s'%d)
427 def node_matches(self, match, colspan):
428 ''' display the files and messages for a node that matched a
429 full text search
430 '''
431 w = self.client.write
432 db = self.client.db
433 message_links = []
434 file_links = []
435 if match.has_key('messages'):
436 for msgid in match['messages']:
437 k = db.msg.labelprop(1)
438 lab = db.msg.get(msgid, k)
439 msgpath = 'msg%s'%msgid
440 message_links.append('<a href="%(msgpath)s">%(lab)s</a>'
441 %locals())
442 w(_('<tr class="row-hilite"><td colspan="%s">'
443 ' Matched messages: %s</td></tr>\n')%(
444 colspan, ', '.join(message_links)))
446 if match.has_key('files'):
447 for fileid in match['files']:
448 filename = db.file.get(fileid, 'name')
449 filepath = 'file%s/%s'%(fileid, filename)
450 file_links.append('<a href="%(filepath)s">%(filename)s</a>'
451 %locals())
452 w(_('<tr class="row-hilite"><td colspan="%s">'
453 ' Matched files: %s</td></tr>\n')%(
454 colspan, ', '.join(file_links)))
456 def filter_form(self, search_text, filter, columns, group, all_columns,
457 sort, filterspec, pagesize):
458 sortspec = {}
459 for i in range(len(sort)):
460 mod = ''
461 colnm = sort[i]
462 if colnm[0] == '-':
463 mod = '-'
464 colnm = colnm[1:]
465 sortspec[colnm] = '%d%s' % (i+1, mod)
467 startwith = 0
468 rslt = []
469 w = rslt.append
471 # display the filter section
472 w( '<br>')
473 w( '<table border=0 cellspacing=0 cellpadding=1>')
474 w( '<tr class="list-header">')
475 w(_(' <th align="left" colspan="7">Filter specification...</th>'))
476 w( '</tr>')
477 # see if we have any indexed properties
478 if self.client.classname in self.client.db.config.HEADER_SEARCH_LINKS:
479 w('<tr class="location-bar">')
480 w(' <td align="right" class="form-label"><b>Search Terms</b></td>')
481 w(' <td colspan=6 class="form-text"> '
482 '<input type="text"name="search_text" value="%s" size="50">'
483 '</td>'%search_text)
484 w('</tr>')
485 w( '<tr class="location-bar">')
486 w( ' <th align="center" width="20%"> </th>')
487 w(_(' <th align="center" width="10%">Show</th>'))
488 w(_(' <th align="center" width="10%">Group</th>'))
489 w(_(' <th align="center" width="10%">Sort</th>'))
490 w(_(' <th colspan="3" align="center">Condition</th>'))
491 w( '</tr>')
493 properties = self.client.db.getclass(self.classname).getprops()
494 all_columns = properties.keys()
495 all_columns.sort()
496 for nm in all_columns:
497 propdescr = properties.get(nm, None)
498 if not propdescr:
499 print "hey sysadmin - %s is not a property of %r" % (nm, self.classname)
500 continue
501 w( '<tr class="location-bar">')
502 w(_(' <td align="right" class="form-label"><b>%s</b></td>' % nm.capitalize()))
503 # show column - can't show multilinks
504 if isinstance(propdescr, hyperdb.Multilink):
505 w(' <td></td>')
506 else:
507 checked = columns and nm in columns or 0
508 checked = ('', 'checked')[checked]
509 w(' <td align="center" class="form-text"><input type="checkbox" name=":columns"'
510 'value="%s" %s></td>' % (nm, checked) )
511 # can only group on Link
512 if isinstance(propdescr, hyperdb.Link):
513 checked = group and nm in group or 0
514 checked = ('', 'checked')[checked]
515 w(' <td align="center" class="form-text"><input type="checkbox" name=":group"'
516 'value="%s" %s></td>' % (nm, checked) )
517 else:
518 w(' <td></td>')
519 # sort - no sort on Multilinks
520 if isinstance(propdescr, hyperdb.Multilink):
521 w('<td></td>')
522 else:
523 val = sortspec.get(nm, '')
524 w('<td align="center" class="form-text"><input type="text" name=":%s_ss" size="3"'
525 'value="%s"></td>' % (nm,val))
526 # condition
527 val = ''
528 if isinstance(propdescr, hyperdb.Link):
529 op = "is in "
530 xtra = '<a href="javascript:help_window(\'classhelp?classname=%s&properties=id,%s\', \'200\', \'400\')"><b>(list)</b></a>' \
531 % (propdescr.classname, self.client.db.getclass(propdescr.classname).labelprop())
532 val = ','.join(filterspec.get(nm, ''))
533 elif isinstance(propdescr, hyperdb.Multilink):
534 op = "contains "
535 xtra = '<a href="javascript:help_window(\'classhelp?classname=%s&properties=id,%s\', \'200\', \'400\')"><b>(list)</b></a>' \
536 % (propdescr.classname, self.client.db.getclass(propdescr.classname).labelprop())
537 val = ','.join(filterspec.get(nm, ''))
538 elif isinstance(propdescr, hyperdb.String) and nm != 'id':
539 op = "equals "
540 xtra = ""
541 val = filterspec.get(nm, '')
542 elif isinstance(propdescr, hyperdb.Boolean):
543 op = "is "
544 xtra = ""
545 val = filterspec.get(nm, None)
546 if val is not None:
547 val = 'True' and val or 'False'
548 else:
549 val = ''
550 elif isinstance(propdescr, hyperdb.Number):
551 op = "equals "
552 xtra = ""
553 val = str(filterspec.get(nm, ''))
554 else:
555 w('<td></td><td></td><td></td></tr>')
556 continue
557 checked = filter and nm in filter or 0
558 checked = ('', 'checked')[checked]
559 w( ' <td class="form-text"><input type="checkbox" name=":filter" value="%s" %s></td>' \
560 % (nm, checked))
561 w(_(' <td class="form-label" nowrap>%s</td><td class="form-text" nowrap>'
562 '<input type="text" name=":%s_fs" value="%s" size=50>%s</td>' % (op, nm, val, xtra)))
563 w( '</tr>')
564 w('<tr class="location-bar">')
565 w(' <td colspan=7><hr></td>')
566 w('</tr>')
567 w('<tr class="location-bar">')
568 w(_(' <td align="right" class="form-label">Pagesize</td>'))
569 w(' <td colspan=2 align="center" class="form-text"><input type="text" name=":pagesize"'
570 'size="3" value="%s"></td>' % pagesize)
571 w(' <td colspan=4></td>')
572 w('</tr>')
573 w('<tr class="location-bar">')
574 w(_(' <td align="right" class="form-label">Start With</td>'))
575 w(' <td colspan=2 align="center" class="form-text"><input type="text" name=":startwith"'
576 'size="3" value="%s"></td>' % startwith)
577 w(' <td colspan=3></td>')
578 w(' <td></td>')
579 w('</tr>')
580 w('<input type=hidden name=":advancedsearch" value="1">')
582 return '\n'.join(rslt)
584 def simple_filter_form(self, search_text, filter, columns, group, all_columns,
585 sort, filterspec, pagesize):
587 startwith = 0
588 rslt = []
589 w = rslt.append
591 # display the filter section
592 w( '<br>')
593 w( '<table border=0 cellspacing=0 cellpadding=1>')
594 w( '<tr class="list-header">')
595 w(_(' <th align="left" colspan="7">Query modifications...</th>'))
596 w( '</tr>')
598 if group:
599 selectedgroup = group[0]
600 groupopts = ['<select name=":group">','<option value="">--no selection--</option>']
601 else:
602 selectedgroup = None
603 groupopts = ['<select name=":group">','<option value="" selected>--no selection--</option>']
604 descending = 0
605 if sort:
606 selectedsort = sort[0]
607 if selectedsort[0] == '-':
608 selectedsort = selectedsort[1:]
609 descending = 1
610 sortopts = ['<select name=":sort">', '<option value="">--no selection--</option>']
611 else:
612 selectedsort = None
613 sortopts = ['<select name=":sort">', '<option value="" selected>--no selection--</option>']
615 for nm in all_columns:
616 propdescr = self.client.db.getclass(self.client.classname).getprops().get(nm, None)
617 if not propdescr:
618 print "hey sysadmin - %s is not a property of %r" % (nm, self.classname)
619 continue
620 if isinstance(propdescr, hyperdb.Link):
621 selected = ''
622 if nm == selectedgroup:
623 selected = 'selected'
624 groupopts.append('<option value="%s" %s>%s</option>' % (nm, selected, nm.capitalize()))
625 selected = ''
626 if nm == selectedsort:
627 selected = 'selected'
628 sortopts.append('<option value="%s" %s>%s</option>' % (nm, selected, nm.capitalize()))
629 if len(groupopts) > 2:
630 groupopts.append('</select>')
631 groupopts = '\n'.join(groupopts)
632 w('<tr class="location-bar">')
633 w(' <td align="right" class="form-label"><b>Group</b></td>')
634 w(' <td class="form-text">%s</td>' % groupopts)
635 w('</tr>')
636 if len(sortopts) > 2:
637 sortopts.append('</select>')
638 sortopts = '\n'.join(sortopts)
639 w('<tr class="location-bar">')
640 w(' <td align="right" class="form-label"><b>Sort</b></td>')
641 checked = descending and 'checked' or ''
642 w(' <td class="form-text">%s <span class="form-label">Descending</span>'
643 '<input type=checkbox name=":descending" value="1" %s></td>' % (sortopts, checked))
644 w('</tr>')
645 w('<input type=hidden name="search_text" value="%s">' % urllib.quote(search_text))
646 w('<input type=hidden name=":filter" value="%s">' % ','.join(filter))
647 w('<input type=hidden name=":columns" value="%s">' % ','.join(columns))
648 for nm in filterspec.keys():
649 w('<input type=hidden name=":%s_fs" value="%s">' % (nm, ','.join(filterspec[nm])))
650 w('<input type=hidden name=":pagesize" value="%s">' % pagesize)
652 return '\n'.join(rslt)
654 def filter_section(self, search_text, filter, columns, group, all_columns,
655 sort, filterspec, pagesize, startwith, simpleform=1):
656 w = self.client.write
657 if simpleform:
658 w(self.simple_filter_form(search_text, filter, columns, group,
659 all_columns, sort, filterspec, pagesize))
660 else:
661 w(self.filter_form(search_text, filter, columns, group, all_columns,
662 sort, filterspec, pagesize))
663 w(' <tr class="location-bar">\n')
664 w(' <td colspan=7><hr></td>\n')
665 w(' </tr>\n')
666 w(' <tr class="location-bar">\n')
667 w(' <td> </td>\n')
668 w(' <td colspan=6><input type="submit" name="Query" value="Redisplay"></td>\n')
669 w(' </tr>\n')
670 if (not simpleform
671 and self.client.db.getclass('user').getprops().has_key('queries')
672 and not self.client.user in (None, "anonymous")):
673 w(' <tr class="location-bar">\n')
674 w(' <td colspan=7><hr></td>\n')
675 w(' </tr>\n')
676 w(' <tr class="location-bar">\n')
677 w(' <td align=right class="form-label">Name</td>\n')
678 w(' <td colspan=2 class="form-text"><input type="text" name=":name" value=""></td>\n')
679 w(' <td colspan=4 rowspan=2 class="form-help">If you give the query a name '
680 'and click <b>Save</b>, it will appear on your menu. Saved queries may be '
681 'edited by going to <b>My Details</b> and clicking on the query name.</td>')
682 w(' </tr>\n')
683 w(' <tr class="location-bar">\n')
684 w(' <td> </td><input type="hidden" name=":classname" value="%s">\n' % self.classname)
685 w(' <td colspan=2><input type="submit" name="Query" value="Save"></td>\n')
686 w(' </tr>\n')
687 w('</table>\n')
689 def sortby(self, sort_name, search_text, filterspec, columns, filter,
690 group, sort, pagesize):
691 ''' Figure the link for a column heading so we can sort by that
692 column
693 '''
694 l = []
695 w = l.append
696 if search_text:
697 w('search_text=%s' % search_text)
698 for k, v in filterspec.items():
699 k = urllib.quote(k)
700 if type(v) == type([]):
701 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
702 else:
703 w('%s=%s'%(k, urllib.quote(v)))
704 if columns:
705 w(':columns=%s'%','.join(map(urllib.quote, columns)))
706 if filter:
707 w(':filter=%s'%','.join(map(urllib.quote, filter)))
708 if group:
709 w(':group=%s'%','.join(map(urllib.quote, group)))
710 w(':pagesize=%s' % pagesize)
711 w(':startwith=0')
713 # handle the sorting - if we're already sorting by this column,
714 # then reverse the sorting, otherwise set the sorting to be this
715 # column only
716 sorting = None
717 if len(sort) == 1:
718 name = sort[0]
719 dir = name[0]
720 if dir == '-' and name[1:] == sort_name:
721 sorting = ':sort=%s'%sort_name
722 elif name == sort_name:
723 sorting = ':sort=-%s'%sort_name
724 if sorting is None:
725 sorting = ':sort=%s'%sort_name
726 w(sorting)
728 return '&'.join(l)
730 class ItemTemplate(Template):
731 ''' show one node as a form '''
732 extension = '.item'
733 def __init__(self, client, templates, classname):
734 Template.__init__(self, client, templates, classname)
735 self.nodeid = client.nodeid
736 def render(self, nodeid):
737 try:
738 cl = self.cl
739 properties = self.properties
740 if (properties.has_key('type') and
741 properties.has_key('content')):
742 pass
743 # XXX we really want to return this as a downloadable...
744 # currently I handle this at a higher level by detecting 'file'
745 # designators...
747 w = self.client.write
748 w('<form onSubmit="return submit_once()" action="%s%s" '
749 'method="POST" enctype="multipart/form-data">'%(self.classname,
750 nodeid))
751 try:
752 self._render()
753 except:
754 etype = sys.exc_type
755 if type(etype) is types.ClassType:
756 etype = etype.__name__
757 w('<p class="system-msg">%s: %s</p>'%(etype, sys.exc_value))
758 # make sure we don't commit any changes
759 self.client.db.rollback()
760 w('</form>')
761 finally:
762 self.cl = self.properties = self.client = None
764 class NewItemTemplate(Template):
765 ''' display a form for creating a new node '''
766 extension = '.newitem'
767 fallbackextension = '.item'
768 def __init__(self, client, templates, classname):
769 Template.__init__(self, client, templates, classname)
770 def render(self, form):
771 try:
772 self.form = form
773 w = self.client.write
774 c = self.client.classname
775 w('<form onSubmit="return submit_once()" action="new%s" '
776 'method="POST" enctype="multipart/form-data">'%c)
777 for key in form.keys():
778 if key[0] == ':':
779 value = form[key].value
780 if type(value) != type([]): value = [value]
781 for value in value:
782 w('<input type="hidden" name="%s" value="%s">'%(key,
783 value))
784 self._render()
785 w('</form>')
786 finally:
787 self.cl = self.properties = self.client = None
789 def splitargs(*args, **kws):
790 return args, kws
791 # [('permission', 'perm2,perm3'), ('assignedto', '$userid'), ('status', 'open')]
793 templatefuncs = {}
794 for nm in template_funcs.__dict__.keys():
795 if nm.startswith('do_'):
796 templatefuncs[nm[3:]] = getattr(template_funcs, nm)
798 #
799 # $Log: not supported by cvs2svn $
800 # Revision 1.110 2002/08/13 20:16:09 gmcm
801 # Use a real parser for templates.
802 # Rewrite htmltemplate to use the parser (hack, hack).
803 # Move the "do_XXX" methods to template_funcs.py.
804 # Redo the funcion tests (but not Template tests - they're hopeless).
805 # Simplified query form in cgi_client.
806 # Ability to delete msgs, files, queries.
807 # Ability to edit the metadata on files.
808 #
809 # Revision 1.109 2002/08/01 15:06:08 gmcm
810 # Use same regex to split search terms as used to index text.
811 # Fix to back_metakit for not changing journaltag on reopen.
812 # Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
813 # Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
814 #
815 # Revision 1.108 2002/07/31 22:40:50 gmcm
816 # Fixes to the search form and saving queries.
817 # Fixes to sorting in back_metakit.py.
818 #
819 # Revision 1.107 2002/07/30 05:27:30 richard
820 # nicer error messages, and a bugfix
821 #
822 # Revision 1.106 2002/07/30 02:41:04 richard
823 # Removed the confusing, ugly two-column sorting stuff. Column heading clicks
824 # now only sort on one column. Nice and simple and obvious.
825 #
826 # Revision 1.105 2002/07/26 08:26:59 richard
827 # Very close now. The cgi and mailgw now use the new security API. The two
828 # templates have been migrated to that setup. Lots of unit tests. Still some
829 # issue in the web form for editing Roles assigned to users.
830 #
831 # Revision 1.104 2002/07/25 07:14:05 richard
832 # Bugger it. Here's the current shape of the new security implementation.
833 # Still to do:
834 # . call the security funcs from cgi and mailgw
835 # . change shipped templates to include correct initialisation and remove
836 # the old config vars
837 # ... that seems like a lot. The bulk of the work has been done though. Honest :)
838 #
839 # Revision 1.103 2002/07/20 19:29:10 gmcm
840 # Fixes/improvements to the search form & saved queries.
841 #
842 # Revision 1.102 2002/07/18 23:07:08 richard
843 # Unit tests and a few fixes.
844 #
845 # Revision 1.101 2002/07/18 11:17:30 gmcm
846 # Add Number and Boolean types to hyperdb.
847 # Add conversion cases to web, mail & admin interfaces.
848 # Add storage/serialization cases to back_anydbm & back_metakit.
849 #
850 # Revision 1.100 2002/07/18 07:01:54 richard
851 # minor bugfix
852 #
853 # Revision 1.99 2002/07/17 12:39:10 gmcm
854 # Saving, running & editing queries.
855 #
856 # Revision 1.98 2002/07/10 00:17:46 richard
857 # . added sorting of checklist HTML display
858 #
859 # Revision 1.97 2002/07/09 05:20:09 richard
860 # . added email display function - mangles email addrs so they're not so easily
861 # scraped from the web
862 #
863 # Revision 1.96 2002/07/09 04:19:09 richard
864 # Added reindex command to roundup-admin.
865 # Fixed reindex on first access.
866 # Also fixed reindexing of entries that change.
867 #
868 # Revision 1.95 2002/07/08 15:32:06 gmcm
869 # Pagination of index pages.
870 # New search form.
871 #
872 # Revision 1.94 2002/06/27 15:38:53 gmcm
873 # Fix the cycles (a clear method, called after render, that removes
874 # the bound methods from the globals dict).
875 # Use cl.filter instead of cl.list followed by sortfunc. For some
876 # backends (Metakit), filter can sort at C speeds, cutting >10 secs
877 # off of filling in the <select...> box for assigned_to when you
878 # have 600+ users.
879 #
880 # Revision 1.93 2002/06/27 12:05:25 gmcm
881 # Default labelprops to id.
882 # In history, make sure there's a .item before making a link / multilink into an href.
883 # Also in history, cgi.escape String properties.
884 # Clean up some of the reference cycles.
885 #
886 # Revision 1.92 2002/06/11 04:57:04 richard
887 # Added optional additional property to display in a Multilink form menu.
888 #
889 # Revision 1.91 2002/05/31 00:08:02 richard
890 # can now just display a link/multilink id - useful for stylesheet stuff
891 #
892 # Revision 1.90 2002/05/25 07:16:24 rochecompaan
893 # Merged search_indexing-branch with HEAD
894 #
895 # Revision 1.89 2002/05/15 06:34:47 richard
896 # forgot to fix the templating for last change
897 #
898 # Revision 1.88 2002/04/24 08:34:35 rochecompaan
899 # Sorting was applied to all nodes of the MultiLink class instead of
900 # the nodes that are actually linked to in the "field" template
901 # function. This adds about 20+ seconds in the display of an issue if
902 # your database has a 1000 or more issue in it.
903 #
904 # Revision 1.87 2002/04/03 06:12:46 richard
905 # Fix for date properties as labels.
906 #
907 # Revision 1.86 2002/04/03 05:54:31 richard
908 # Fixed serialisation problem by moving the serialisation step out of the
909 # hyperdb.Class (get, set) into the hyperdb.Database.
910 #
911 # Also fixed htmltemplate after the showid changes I made yesterday.
912 #
913 # Unit tests for all of the above written.
914 #
915 # Revision 1.85 2002/04/02 01:40:58 richard
916 # . link() htmltemplate function now has a "showid" option for links and
917 # multilinks. When true, it only displays the linked node id as the anchor
918 # text. The link value is displayed as a tooltip using the title anchor
919 # attribute.
920 #
921 # Revision 1.84.2.2 2002/04/20 13:23:32 rochecompaan
922 # We now have a separate search page for nodes. Search links for
923 # different classes can be customized in instance_config similar to
924 # index links.
925 #
926 # Revision 1.84.2.1 2002/04/19 19:54:42 rochecompaan
927 # cgi_client.py
928 # removed search link for the time being
929 # moved rendering of matches to htmltemplate
930 # hyperdb.py
931 # filtering of nodes on full text search incorporated in filter method
932 # roundupdb.py
933 # added paramater to call of filter method
934 # roundup_indexer.py
935 # added search method to RoundupIndexer class
936 #
937 # Revision 1.84 2002/03/29 19:41:48 rochecompaan
938 # . Fixed display of mutlilink properties when using the template
939 # functions, menu and plain.
940 #
941 # Revision 1.83 2002/02/27 04:14:31 richard
942 # Ran it through pychecker, made fixes
943 #
944 # Revision 1.82 2002/02/21 23:11:45 richard
945 # . fixed some problems in date calculations (calendar.py doesn't handle over-
946 # and under-flow). Also, hour/minute/second intervals may now be more than
947 # 99 each.
948 #
949 # Revision 1.81 2002/02/21 07:21:38 richard
950 # docco
951 #
952 # Revision 1.80 2002/02/21 07:19:08 richard
953 # ... and label, width and height control for extra flavour!
954 #
955 # Revision 1.79 2002/02/21 06:57:38 richard
956 # . Added popup help for classes using the classhelp html template function.
957 # - add <display call="classhelp('priority', 'id,name,description')">
958 # to an item page, and it generates a link to a popup window which displays
959 # the id, name and description for the priority class. The description
960 # field won't exist in most installations, but it will be added to the
961 # default templates.
962 #
963 # Revision 1.78 2002/02/21 06:23:00 richard
964 # *** empty log message ***
965 #
966 # Revision 1.77 2002/02/20 05:05:29 richard
967 # . Added simple editing for classes that don't define a templated interface.
968 # - access using the admin "class list" interface
969 # - limited to admin-only
970 # - requires the csv module from object-craft (url given if it's missing)
971 #
972 # Revision 1.76 2002/02/16 09:10:52 richard
973 # oops
974 #
975 # Revision 1.75 2002/02/16 08:43:23 richard
976 # . #517906 ] Attribute order in "View customisation"
977 #
978 # Revision 1.74 2002/02/16 08:39:42 richard
979 # . #516854 ] "My Issues" and redisplay
980 #
981 # Revision 1.73 2002/02/15 07:08:44 richard
982 # . Alternate email addresses are now available for users. See the MIGRATION
983 # file for info on how to activate the feature.
984 #
985 # Revision 1.72 2002/02/14 23:39:18 richard
986 # . All forms now have "double-submit" protection when Javascript is enabled
987 # on the client-side.
988 #
989 # Revision 1.71 2002/01/23 06:15:24 richard
990 # real (non-string, duh) sorting of lists by node id
991 #
992 # Revision 1.70 2002/01/23 05:47:57 richard
993 # more HTML template cleanup and unit tests
994 #
995 # Revision 1.69 2002/01/23 05:10:27 richard
996 # More HTML template cleanup and unit tests.
997 # - download() now implemented correctly, replacing link(is_download=1) [fixed in the
998 # templates, but link(is_download=1) will still work for existing templates]
999 #
1000 # Revision 1.68 2002/01/22 22:55:28 richard
1001 # . htmltemplate list() wasn't sorting...
1002 #
1003 # Revision 1.67 2002/01/22 22:46:22 richard
1004 # more htmltemplate cleanups and unit tests
1005 #
1006 # Revision 1.66 2002/01/22 06:35:40 richard
1007 # more htmltemplate tests and cleanup
1008 #
1009 # Revision 1.65 2002/01/22 00:12:06 richard
1010 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1011 # off the implementation of some of the functions so they behave sanely.
1012 #
1013 # Revision 1.64 2002/01/21 03:25:59 richard
1014 # oops
1015 #
1016 # Revision 1.63 2002/01/21 02:59:10 richard
1017 # Fixed up the HTML display of history so valid links are actually displayed.
1018 # Oh for some unit tests! :(
1019 #
1020 # Revision 1.62 2002/01/18 08:36:12 grubert
1021 # . add nowrap to history table date cell i.e. <td nowrap ...
1022 #
1023 # Revision 1.61 2002/01/17 23:04:53 richard
1024 # . much nicer history display (actualy real handling of property types etc)
1025 #
1026 # Revision 1.60 2002/01/17 08:48:19 grubert
1027 # . display superseder as html link in history.
1028 #
1029 # Revision 1.59 2002/01/17 07:58:24 grubert
1030 # . display links a html link in history.
1031 #
1032 # Revision 1.58 2002/01/15 00:50:03 richard
1033 # #502949 ] index view for non-issues and redisplay
1034 #
1035 # Revision 1.57 2002/01/14 23:31:21 richard
1036 # reverted the change that had plain() hyperlinking the link displays -
1037 # that's what link() is for!
1038 #
1039 # Revision 1.56 2002/01/14 07:04:36 richard
1040 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1041 # the linked node's page.
1042 # ... this allows a display very similar to bugzilla's where you can actually
1043 # find out information about the linked node.
1044 #
1045 # Revision 1.55 2002/01/14 06:45:03 richard
1046 # . #502953 ] nosy-like treatment of other multilinks
1047 # ... had to revert most of the previous change to the multilink field
1048 # display... not good.
1049 #
1050 # Revision 1.54 2002/01/14 05:16:51 richard
1051 # The submit buttons need a name attribute or mozilla won't submit without a
1052 # file upload. Yeah, that's bloody obscure. Grr.
1053 #
1054 # Revision 1.53 2002/01/14 04:03:32 richard
1055 # How about that ... date fields have never worked ...
1056 #
1057 # Revision 1.52 2002/01/14 02:20:14 richard
1058 # . changed all config accesses so they access either the instance or the
1059 # config attriubute on the db. This means that all config is obtained from
1060 # instance_config instead of the mish-mash of classes. This will make
1061 # switching to a ConfigParser setup easier too, I hope.
1062 #
1063 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1064 # 0.5.0 switch, I hope!)
1065 #
1066 # Revision 1.51 2002/01/10 10:02:15 grubert
1067 # In do_history: replace "." in date by " " so html wraps more sensible.
1068 # Should this be done in date's string converter ?
1069 #
1070 # Revision 1.50 2002/01/05 02:35:10 richard
1071 # I18N'ification
1072 #
1073 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1074 # Features added:
1075 # . Multilink properties are now displayed as comma separated values in
1076 # a textbox
1077 # . The add user link is now only visible to the admin user
1078 # . Modified the mail gateway to reject submissions from unknown
1079 # addresses if ANONYMOUS_ACCESS is denied
1080 #
1081 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1082 # Bugs fixed:
1083 # . Exception handling in hyperdb for strings-that-look-like numbers got
1084 # lost somewhere
1085 # . Internet Explorer submits full path for filename - we now strip away
1086 # the path
1087 # Features added:
1088 # . Link and multilink properties are now displayed sorted in the cgi
1089 # interface
1090 #
1091 # Revision 1.47 2001/11/26 22:55:56 richard
1092 # Feature:
1093 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1094 # the instance.
1095 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1096 # signature info in e-mails.
1097 # . Some more flexibility in the mail gateway and more error handling.
1098 # . Login now takes you to the page you back to the were denied access to.
1099 #
1100 # Fixed:
1101 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1102 #
1103 # Revision 1.46 2001/11/24 00:53:12 jhermann
1104 # "except:" is bad, bad , bad!
1105 #
1106 # Revision 1.45 2001/11/22 15:46:42 jhermann
1107 # Added module docstrings to all modules.
1108 #
1109 # Revision 1.44 2001/11/21 23:35:45 jhermann
1110 # Added globbing for win32, and sample marking in a 2nd file to test it
1111 #
1112 # Revision 1.43 2001/11/21 04:04:43 richard
1113 # *sigh* more missing value handling
1114 #
1115 # Revision 1.42 2001/11/21 03:40:54 richard
1116 # more new property handling
1117 #
1118 # Revision 1.41 2001/11/15 10:26:01 richard
1119 # . missing "return" in filter_section (thanks Roch'e Compaan)
1120 #
1121 # Revision 1.40 2001/11/03 01:56:51 richard
1122 # More HTML compliance fixes. This will probably fix the Netscape problem
1123 # too.
1124 #
1125 # Revision 1.39 2001/11/03 01:43:47 richard
1126 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1127 #
1128 # Revision 1.38 2001/10/31 06:58:51 richard
1129 # Added the wrap="hard" attribute to the textarea of the note field so the
1130 # messages wrap sanely.
1131 #
1132 # Revision 1.37 2001/10/31 06:24:35 richard
1133 # Added do_stext to htmltemplate, thanks Brad Clements.
1134 #
1135 # Revision 1.36 2001/10/28 22:51:38 richard
1136 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1137 #
1138 # Revision 1.35 2001/10/24 00:04:41 richard
1139 # Removed the "infinite authentication loop", thanks Roch'e
1140 #
1141 # Revision 1.34 2001/10/23 22:56:36 richard
1142 # Bugfix in filter "widget" placement, thanks Roch'e
1143 #
1144 # Revision 1.33 2001/10/23 01:00:18 richard
1145 # Re-enabled login and registration access after lopping them off via
1146 # disabling access for anonymous users.
1147 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1148 # a couple of bugs while I was there. Probably introduced a couple, but
1149 # things seem to work OK at the moment.
1150 #
1151 # Revision 1.32 2001/10/22 03:25:01 richard
1152 # Added configuration for:
1153 # . anonymous user access and registration (deny/allow)
1154 # . filter "widget" location on index page (top, bottom, both)
1155 # Updated some documentation.
1156 #
1157 # Revision 1.31 2001/10/21 07:26:35 richard
1158 # feature #473127: Filenames. I modified the file.index and htmltemplate
1159 # source so that the filename is used in the link and the creation
1160 # information is displayed.
1161 #
1162 # Revision 1.30 2001/10/21 04:44:50 richard
1163 # bug #473124: UI inconsistency with Link fields.
1164 # This also prompted me to fix a fairly long-standing usability issue -
1165 # that of being able to turn off certain filters.
1166 #
1167 # Revision 1.29 2001/10/21 00:17:56 richard
1168 # CGI interface view customisation section may now be hidden (patch from
1169 # Roch'e Compaan.)
1170 #
1171 # Revision 1.28 2001/10/21 00:00:16 richard
1172 # Fixed Checklist function - wasn't always working on a list.
1173 #
1174 # Revision 1.27 2001/10/20 12:13:44 richard
1175 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1176 #
1177 # Revision 1.26 2001/10/14 10:55:00 richard
1178 # Handle empty strings in HTML template Link function
1179 #
1180 # Revision 1.25 2001/10/09 07:25:59 richard
1181 # Added the Password property type. See "pydoc roundup.password" for
1182 # implementation details. Have updated some of the documentation too.
1183 #
1184 # Revision 1.24 2001/09/27 06:45:58 richard
1185 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1186 # on the plain() template function to escape the text for HTML.
1187 #
1188 # Revision 1.23 2001/09/10 09:47:18 richard
1189 # Fixed bug in the generation of links to Link/Multilink in indexes.
1190 # (thanks Hubert Hoegl)
1191 # Added AssignedTo to the "classic" schema's item page.
1192 #
1193 # Revision 1.22 2001/08/30 06:01:17 richard
1194 # Fixed missing import in mailgw :(
1195 #
1196 # Revision 1.21 2001/08/16 07:34:59 richard
1197 # better CGI text searching - but hidden filter fields are disappearing...
1198 #
1199 # Revision 1.20 2001/08/15 23:43:18 richard
1200 # Fixed some isFooTypes that I missed.
1201 # Refactored some code in the CGI code.
1202 #
1203 # Revision 1.19 2001/08/12 06:32:36 richard
1204 # using isinstance(blah, Foo) now instead of isFooType
1205 #
1206 # Revision 1.18 2001/08/07 00:24:42 richard
1207 # stupid typo
1208 #
1209 # Revision 1.17 2001/08/07 00:15:51 richard
1210 # Added the copyright/license notice to (nearly) all files at request of
1211 # Bizar Software.
1212 #
1213 # Revision 1.16 2001/08/01 03:52:23 richard
1214 # Checklist was using wrong name.
1215 #
1216 # Revision 1.15 2001/07/30 08:12:17 richard
1217 # Added time logging and file uploading to the templates.
1218 #
1219 # Revision 1.14 2001/07/30 06:17:45 richard
1220 # Features:
1221 # . Added ability for cgi newblah forms to indicate that the new node
1222 # should be linked somewhere.
1223 # Fixed:
1224 # . Fixed the agument handling for the roundup-admin find command.
1225 # . Fixed handling of summary when no note supplied for newblah. Again.
1226 # . Fixed detection of no form in htmltemplate Field display.
1227 #
1228 # Revision 1.13 2001/07/30 02:37:53 richard
1229 # Temporary measure until we have decent schema migration.
1230 #
1231 # Revision 1.12 2001/07/30 01:24:33 richard
1232 # Handles new node display now.
1233 #
1234 # Revision 1.11 2001/07/29 09:31:35 richard
1235 # oops
1236 #
1237 # Revision 1.10 2001/07/29 09:28:23 richard
1238 # Fixed sorting by clicking on column headings.
1239 #
1240 # Revision 1.9 2001/07/29 08:27:40 richard
1241 # Fixed handling of passed-in values in form elements (ie. during a
1242 # drill-down)
1243 #
1244 # Revision 1.8 2001/07/29 07:01:39 richard
1245 # Added vim command to all source so that we don't get no steenkin' tabs :)
1246 #
1247 # Revision 1.7 2001/07/29 05:36:14 richard
1248 # Cleanup of the link label generation.
1249 #
1250 # Revision 1.6 2001/07/29 04:06:42 richard
1251 # Fixed problem in link display when Link value is None.
1252 #
1253 # Revision 1.5 2001/07/28 08:17:09 richard
1254 # fixed use of stylesheet
1255 #
1256 # Revision 1.4 2001/07/28 07:59:53 richard
1257 # Replaced errno integers with their module values.
1258 # De-tabbed templatebuilder.py
1259 #
1260 # Revision 1.3 2001/07/25 03:39:47 richard
1261 # Hrm - displaying links to classes that don't specify a key property. I've
1262 # got it defaulting to 'name', then 'title' and then a "random" property (first
1263 # one returned by getprops().keys().
1264 # Needs to be moved onto the Class I think...
1265 #
1266 # Revision 1.2 2001/07/22 12:09:32 richard
1267 # Final commit of Grande Splite
1268 #
1269 # Revision 1.1 2001/07/22 11:58:35 richard
1270 # More Grande Splite
1271 #
1272 #
1273 # vim: set filetype=python ts=4 sw=4 et si