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