c65ef4dea6763cd0ad5db16f6df981b888585938
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.29 2001-10-21 00:17:56 richard Exp $
20 import os, re, StringIO, urllib, cgi, errno
22 import hyperdb, date, password
24 class Base:
25 def __init__(self, db, templates, classname, nodeid=None, form=None,
26 filterspec=None):
27 # TODO: really not happy with the way templates is passed on here
28 self.db, self.templates = db, templates
29 self.classname, self.nodeid = classname, nodeid
30 self.form, self.filterspec = form, filterspec
31 self.cl = self.db.classes[self.classname]
32 self.properties = self.cl.getprops()
34 class Plain(Base):
35 ''' display a String property directly;
37 display a Date property in a specified time zone with an option to
38 omit the time from the date stamp;
40 for a Link or Multilink property, display the key strings of the
41 linked nodes (or the ids if the linked class has no key property)
42 '''
43 def __call__(self, property, escape=0):
44 if not self.nodeid and self.form is None:
45 return '[Field: not called from item]'
46 propclass = self.properties[property]
47 if self.nodeid:
48 value = self.cl.get(self.nodeid, property)
49 else:
50 # TODO: pull the value from the form
51 if isinstance(propclass, hyperdb.Multilink): value = []
52 else: value = ''
53 if isinstance(propclass, hyperdb.String):
54 if value is None: value = ''
55 else: value = str(value)
56 elif isinstance(propclass, hyperdb.Password):
57 if value is None: value = ''
58 else: value = '*encrypted*'
59 elif isinstance(propclass, hyperdb.Date):
60 value = str(value)
61 elif isinstance(propclass, hyperdb.Interval):
62 value = str(value)
63 elif isinstance(propclass, hyperdb.Link):
64 linkcl = self.db.classes[propclass.classname]
65 k = linkcl.labelprop()
66 if value: value = str(linkcl.get(value, k))
67 else: value = '[unselected]'
68 elif isinstance(propclass, hyperdb.Multilink):
69 linkcl = self.db.classes[propclass.classname]
70 k = linkcl.labelprop()
71 value = ', '.join([linkcl.get(i, k) for i in value])
72 else:
73 s = 'Plain: bad propclass "%s"'%propclass
74 if escape:
75 return cgi.escape(value)
76 return value
78 class Field(Base):
79 ''' display a property like the plain displayer, but in a text field
80 to be edited
81 '''
82 def __call__(self, property, size=None, height=None, showid=0):
83 if not self.nodeid and self.form is None and self.filterspec is None:
84 return '[Field: not called from item]'
85 propclass = self.properties[property]
86 if self.nodeid:
87 value = self.cl.get(self.nodeid, property, None)
88 # TODO: remove this from the code ... it's only here for
89 # handling schema changes, and they should be handled outside
90 # of this code...
91 if isinstance(propclass, hyperdb.Multilink) and value is None:
92 value = []
93 elif self.filterspec is not None:
94 if isinstance(propclass, hyperdb.Multilink):
95 value = self.filterspec.get(property, [])
96 else:
97 value = self.filterspec.get(property, '')
98 else:
99 # TODO: pull the value from the form
100 if isinstance(propclass, hyperdb.Multilink): value = []
101 else: value = ''
102 if (isinstance(propclass, hyperdb.String) or
103 isinstance(propclass, hyperdb.Date) or
104 isinstance(propclass, hyperdb.Interval)):
105 size = size or 30
106 if value is None:
107 value = ''
108 else:
109 value = cgi.escape(value)
110 value = '"'.join(value.split('"'))
111 s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
112 elif isinstance(propclass, hyperdb.Password):
113 size = size or 30
114 s = '<input type="password" name="%s" size="%s">'%(property, size)
115 elif isinstance(propclass, hyperdb.Link):
116 linkcl = self.db.classes[propclass.classname]
117 l = ['<select name="%s">'%property]
118 k = linkcl.labelprop()
119 for optionid in linkcl.list():
120 option = linkcl.get(optionid, k)
121 s = ''
122 if optionid == value:
123 s = 'selected '
124 if showid:
125 lab = '%s%s: %s'%(propclass.classname, optionid, option)
126 else:
127 lab = option
128 if size is not None and len(lab) > size:
129 lab = lab[:size-3] + '...'
130 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
131 l.append('</select>')
132 s = '\n'.join(l)
133 elif isinstance(propclass, hyperdb.Multilink):
134 linkcl = self.db.classes[propclass.classname]
135 list = linkcl.list()
136 height = height or min(len(list), 7)
137 l = ['<select multiple name="%s" size="%s">'%(property, height)]
138 k = linkcl.labelprop()
139 for optionid in list:
140 option = linkcl.get(optionid, k)
141 s = ''
142 if optionid in value:
143 s = 'selected '
144 if showid:
145 lab = '%s%s: %s'%(propclass.classname, optionid, option)
146 else:
147 lab = option
148 if size is not None and len(lab) > size:
149 lab = lab[:size-3] + '...'
150 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
151 l.append('</select>')
152 s = '\n'.join(l)
153 else:
154 s = 'Plain: bad propclass "%s"'%propclass
155 return s
157 class Menu(Base):
158 ''' for a Link property, display a menu of the available choices
159 '''
160 def __call__(self, property, size=None, height=None, showid=0):
161 propclass = self.properties[property]
162 if self.nodeid:
163 value = self.cl.get(self.nodeid, property)
164 else:
165 # TODO: pull the value from the form
166 if isinstance(propclass, hyperdb.Multilink): value = []
167 else: value = None
168 if isinstance(propclass, hyperdb.Link):
169 linkcl = self.db.classes[propclass.classname]
170 l = ['<select name="%s">'%property]
171 k = linkcl.labelprop()
172 for optionid in linkcl.list():
173 option = linkcl.get(optionid, k)
174 s = ''
175 if optionid == value:
176 s = 'selected '
177 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
178 l.append('</select>')
179 return '\n'.join(l)
180 if isinstance(propclass, hyperdb.Multilink):
181 linkcl = self.db.classes[propclass.classname]
182 list = linkcl.list()
183 height = height or min(len(list), 7)
184 l = ['<select multiple name="%s" size="%s">'%(property, height)]
185 k = linkcl.labelprop()
186 for optionid in list:
187 option = linkcl.get(optionid, k)
188 s = ''
189 if optionid in value:
190 s = 'selected '
191 if showid:
192 lab = '%s%s: %s'%(propclass.classname, optionid, option)
193 else:
194 lab = option
195 if size is not None and len(lab) > size:
196 lab = lab[:size-3] + '...'
197 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
198 l.append('</select>')
199 return '\n'.join(l)
200 return '[Menu: not a link]'
202 #XXX deviates from spec
203 class Link(Base):
204 ''' for a Link or Multilink property, display the names of the linked
205 nodes, hyperlinked to the item views on those nodes
206 for other properties, link to this node with the property as the text
207 '''
208 def __call__(self, property=None, **args):
209 if not self.nodeid and self.form is None:
210 return '[Link: not called from item]'
211 propclass = self.properties[property]
212 if self.nodeid:
213 value = self.cl.get(self.nodeid, property)
214 else:
215 if isinstance(propclass, hyperdb.Multilink): value = []
216 elif isinstance(propclass, hyperdb.Link): value = None
217 else: value = ''
218 if isinstance(propclass, hyperdb.Link):
219 linkname = propclass.classname
220 if value is None: return '[no %s]'%property.capitalize()
221 linkcl = self.db.classes[linkname]
222 k = linkcl.labelprop()
223 linkvalue = linkcl.get(value, k)
224 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
225 if isinstance(propclass, hyperdb.Multilink):
226 linkname = propclass.classname
227 linkcl = self.db.classes[linkname]
228 k = linkcl.labelprop()
229 if not value : return '[no %s]'%property.capitalize()
230 l = []
231 for value in value:
232 linkvalue = linkcl.get(value, k)
233 l.append('<a href="%s%s">%s</a>'%(linkname, value, linkvalue))
234 return ', '.join(l)
235 if isinstance(propclass, hyperdb.String):
236 if value == '': value = '[no %s]'%property.capitalize()
237 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
239 class Count(Base):
240 ''' for a Multilink property, display a count of the number of links in
241 the list
242 '''
243 def __call__(self, property, **args):
244 if not self.nodeid:
245 return '[Count: not called from item]'
246 propclass = self.properties[property]
247 value = self.cl.get(self.nodeid, property)
248 if isinstance(propclass, hyperdb.Multilink):
249 return str(len(value))
250 return '[Count: not a Multilink]'
252 # XXX pretty is definitely new ;)
253 class Reldate(Base):
254 ''' display a Date property in terms of an interval relative to the
255 current date (e.g. "+ 3w", "- 2d").
257 with the 'pretty' flag, make it pretty
258 '''
259 def __call__(self, property, pretty=0):
260 if not self.nodeid and self.form is None:
261 return '[Reldate: not called from item]'
262 propclass = self.properties[property]
263 if isinstance(not propclass, hyperdb.Date):
264 return '[Reldate: not a Date]'
265 if self.nodeid:
266 value = self.cl.get(self.nodeid, property)
267 else:
268 value = date.Date('.')
269 interval = value - date.Date('.')
270 if pretty:
271 if not self.nodeid:
272 return 'now'
273 pretty = interval.pretty()
274 if pretty is None:
275 pretty = value.pretty()
276 return pretty
277 return str(interval)
279 class Download(Base):
280 ''' show a Link("file") or Multilink("file") property using links that
281 allow you to download files
282 '''
283 def __call__(self, property, **args):
284 if not self.nodeid:
285 return '[Download: not called from item]'
286 propclass = self.properties[property]
287 value = self.cl.get(self.nodeid, property)
288 if isinstance(propclass, hyperdb.Link):
289 linkcl = self.db.classes[propclass.classname]
290 linkvalue = linkcl.get(value, k)
291 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
292 if isinstance(propclass, hyperdb.Multilink):
293 linkcl = self.db.classes[propclass.classname]
294 l = []
295 for value in value:
296 linkvalue = linkcl.get(value, k)
297 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
298 return ', '.join(l)
299 return '[Download: not a link]'
302 class Checklist(Base):
303 ''' for a Link or Multilink property, display checkboxes for the available
304 choices to permit filtering
305 '''
306 def __call__(self, property, **args):
307 propclass = self.properties[property]
308 if (not isinstance(propclass, hyperdb.Link) and not
309 isinstance(propclass, hyperdb.Multilink)):
310 return '[Checklist: not a link]'
312 # get our current checkbox state
313 if self.nodeid:
314 # get the info from the node - make sure it's a list
315 if isinstance(propclass, hyperdb.Link):
316 value = [self.cl.get(self.nodeid, property)]
317 else:
318 value = self.cl.get(self.nodeid, property)
319 elif self.filterspec is not None:
320 # get the state from the filter specification (always a list)
321 value = self.filterspec.get(property, [])
322 else:
323 # it's a new node, so there's no state
324 value = []
326 # so we can map to the linked node's "lable" property
327 linkcl = self.db.classes[propclass.classname]
328 l = []
329 k = linkcl.labelprop()
330 for optionid in linkcl.list():
331 option = linkcl.get(optionid, k)
332 if optionid in value or option in value:
333 checked = 'checked'
334 else:
335 checked = ''
336 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
337 option, checked, property, option))
338 return '\n'.join(l)
340 class Note(Base):
341 ''' display a "note" field, which is a text area for entering a note to
342 go along with a change.
343 '''
344 def __call__(self, rows=5, cols=80):
345 # TODO: pull the value from the form
346 return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
347 cols)
349 # XXX new function
350 class List(Base):
351 ''' list the items specified by property using the standard index for
352 the class
353 '''
354 def __call__(self, property, reverse=0):
355 propclass = self.properties[property]
356 if isinstance(not propclass, hyperdb.Multilink):
357 return '[List: not a Multilink]'
358 fp = StringIO.StringIO()
359 value = self.cl.get(self.nodeid, property)
360 if reverse:
361 value.reverse()
362 # TODO: really not happy with the way templates is passed on here
363 index(fp, self.templates, self.db, propclass.classname, nodeids=value,
364 show_display_form=0)
365 return fp.getvalue()
367 # XXX new function
368 class History(Base):
369 ''' list the history of the item
370 '''
371 def __call__(self, **args):
372 if self.nodeid is None:
373 return "[History: node doesn't exist]"
375 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
376 '<tr class="list-header">',
377 '<td><span class="list-item"><strong>Date</strong></span></td>',
378 '<td><span class="list-item"><strong>User</strong></span></td>',
379 '<td><span class="list-item"><strong>Action</strong></span></td>',
380 '<td><span class="list-item"><strong>Args</strong></span></td>']
382 for id, date, user, action, args in self.cl.history(self.nodeid):
383 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
384 date, user, action, args))
385 l.append('</table>')
386 return '\n'.join(l)
388 # XXX new function
389 class Submit(Base):
390 ''' add a submit button for the item
391 '''
392 def __call__(self):
393 if self.nodeid:
394 return '<input type="submit" value="Submit Changes">'
395 elif self.form is not None:
396 return '<input type="submit" value="Submit New Entry">'
397 else:
398 return '[Submit: not called from item]'
401 #
402 # INDEX TEMPLATES
403 #
404 class IndexTemplateReplace:
405 def __init__(self, globals, locals, props):
406 self.globals = globals
407 self.locals = locals
408 self.props = props
410 def go(self, text, replace=re.compile(
411 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
412 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
413 return replace.sub(self, text)
415 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
416 if m.group('name'):
417 if m.group('name') in self.props:
418 text = m.group('text')
419 replace = IndexTemplateReplace(self.globals, {}, self.props)
420 return replace.go(m.group('text'))
421 else:
422 return ''
423 if m.group('display'):
424 command = m.group('command')
425 return eval(command, self.globals, self.locals)
426 print '*** unhandled match', m.groupdict()
428 def sortby(sort_name, columns, filter, sort, group, filterspec):
429 l = []
430 w = l.append
431 for k, v in filterspec.items():
432 k = urllib.quote(k)
433 if type(v) == type([]):
434 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
435 else:
436 w('%s=%s'%(k, urllib.quote(v)))
437 if columns:
438 w(':columns=%s'%','.join(map(urllib.quote, columns)))
439 if filter:
440 w(':filter=%s'%','.join(map(urllib.quote, filter)))
441 if group:
442 w(':group=%s'%','.join(map(urllib.quote, group)))
443 m = []
444 s_dir = ''
445 for name in sort:
446 dir = name[0]
447 if dir == '-':
448 name = name[1:]
449 else:
450 dir = ''
451 if sort_name == name:
452 if dir == '-':
453 s_dir = ''
454 else:
455 s_dir = '-'
456 else:
457 m.append(dir+urllib.quote(name))
458 m.insert(0, s_dir+urllib.quote(sort_name))
459 # so things don't get completely out of hand, limit the sort to two columns
460 w(':sort=%s'%','.join(m[:2]))
461 return '&'.join(l)
463 def index(client, templates, db, classname, filterspec={}, filter=[],
464 columns=[], sort=[], group=[], show_display_form=1, nodeids=None,
465 show_customization=1,
466 col_re=re.compile(r'<property\s+name="([^>]+)">')):
467 globals = {
468 'plain': Plain(db, templates, classname, filterspec=filterspec),
469 'field': Field(db, templates, classname, filterspec=filterspec),
470 'menu': Menu(db, templates, classname, filterspec=filterspec),
471 'link': Link(db, templates, classname, filterspec=filterspec),
472 'count': Count(db, templates, classname, filterspec=filterspec),
473 'reldate': Reldate(db, templates, classname, filterspec=filterspec),
474 'download': Download(db, templates, classname, filterspec=filterspec),
475 'checklist': Checklist(db, templates, classname, filterspec=filterspec),
476 'list': List(db, templates, classname, filterspec=filterspec),
477 'history': History(db, templates, classname, filterspec=filterspec),
478 'submit': Submit(db, templates, classname, filterspec=filterspec),
479 'note': Note(db, templates, classname, filterspec=filterspec)
480 }
481 cl = db.classes[classname]
482 properties = cl.getprops()
483 w = client.write
484 w('<form>')
486 try:
487 template = open(os.path.join(templates, classname+'.filter')).read()
488 all_filters = col_re.findall(template)
489 except IOError, error:
490 if error.errno != errno.ENOENT: raise
491 template = None
492 all_filters = []
493 if template and filter:
494 # display the filter section
495 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
496 w('<tr class="location-bar">')
497 w(' <th align="left" colspan="2">Filter specification...</th>')
498 w('</tr>')
499 replace = IndexTemplateReplace(globals, locals(), filter)
500 w(replace.go(template))
501 w('<tr class="location-bar"><td width="1%%"> </td>')
502 w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
503 w('</table>')
505 # If the filters aren't being displayed, then hide their current
506 # value in the form
507 if not filter:
508 for k, v in filterspec.items():
509 if type(v) == type([]): v = ','.join(v)
510 w('<input type="hidden" name="%s" value="%s">'%(k, v))
512 # make sure that the sorting doesn't get lost either
513 if sort:
514 w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
516 # XXX deviate from spec here ...
517 # load the index section template and figure the default columns from it
518 template = open(os.path.join(templates, classname+'.index')).read()
519 all_columns = col_re.findall(template)
520 if not columns:
521 columns = []
522 for name in all_columns:
523 columns.append(name)
524 else:
525 # re-sort columns to be the same order as all_columns
526 l = []
527 for name in all_columns:
528 if name in columns:
529 l.append(name)
530 columns = l
532 # now add in the filter/columns/group/etc config table form
533 w('<input type="hidden" name="show_customization" value="%s">' %
534 show_customization )
535 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
536 names = []
537 for name in cl.getprops().keys():
538 if name in all_filters or name in all_columns:
539 names.append(name)
540 w('<tr class="location-bar">')
541 if show_customization:
542 action = '-'
543 else:
544 action = '+'
545 # hide the values for filters, columns and grouping in the form
546 # if the customization widget is not visible
547 for name in names:
548 if all_filters and name in filter:
549 w('<input type="hidden" name=":filter" value="%s">' % name)
550 if all_columns and name in columns:
551 w('<input type="hidden" name=":columns" value="%s">' % name)
552 if all_columns and name in group:
553 w('<input type="hidden" name=":group" value="%s">' % name)
555 if show_display_form:
556 # TODO: The widget style can go into the stylesheet
557 w('<th align="left" colspan=%s>'
558 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
559 'customisation...</th></tr>\n'%(len(names)+1, action))
560 if show_customization:
561 w('<tr class="location-bar"><th> </th>')
562 for name in names:
563 w('<th>%s</th>'%name.capitalize())
564 w('</tr>\n')
566 # Filter
567 if all_filters:
568 w('<tr><th width="1%" align=right class="location-bar">'
569 'Filters</th>\n')
570 for name in names:
571 if name not in all_filters:
572 w('<td> </td>')
573 continue
574 if name in filter: checked=' checked'
575 else: checked=''
576 w('<td align=middle>\n')
577 w(' <input type="checkbox" name=":filter" value="%s" '
578 '%s></td>\n'%(name, checked))
579 w('</tr>\n')
581 # Columns
582 if all_columns:
583 w('<tr><th width="1%" align=right class="location-bar">'
584 'Columns</th>\n')
585 for name in names:
586 if name not in all_columns:
587 w('<td> </td>')
588 continue
589 if name in columns: checked=' checked'
590 else: checked=''
591 w('<td align=middle>\n')
592 w(' <input type="checkbox" name=":columns" value="%s"'
593 '%s></td>\n'%(name, checked))
594 w('</tr>\n')
596 # Grouping
597 w('<tr><th width="1%" align=right class="location-bar">'
598 'Grouping</th>\n')
599 for name in names:
600 prop = properties[name]
601 if name not in all_columns:
602 w('<td> </td>')
603 continue
604 if name in group: checked=' checked'
605 else: checked=''
606 w('<td align=middle>\n')
607 w(' <input type="checkbox" name=":group" value="%s"'
608 '%s></td>\n'%(name, checked))
609 w('</tr>\n')
611 w('<tr class="location-bar"><td width="1%"> </td>')
612 w('<td colspan="%s">'%len(names))
613 w('<input type="submit" name="action" value="Redisplay"></td>')
614 w('</tr>\n')
616 w('</table>\n')
617 w('</form>\n')
619 # now display the index section
620 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
621 w('<tr class="list-header">\n')
622 for name in columns:
623 cname = name.capitalize()
624 if show_display_form:
625 anchor = "%s?%s"%(classname, sortby(name, columns, filter,
626 sort, group, filterspec))
627 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
628 anchor, cname))
629 else:
630 w('<td><span class="list-header">%s</span></td>\n'%cname)
631 w('</tr>\n')
633 # this stuff is used for group headings - optimise the group names
634 old_group = None
635 group_names = []
636 if group:
637 for name in group:
638 if name[0] == '-': group_names.append(name[1:])
639 else: group_names.append(name)
641 # now actually loop through all the nodes we get from the filter and
642 # apply the template
643 if nodeids is None:
644 nodeids = cl.filter(filterspec, sort, group)
645 for nodeid in nodeids:
646 # check for a group heading
647 if group_names:
648 this_group = [cl.get(nodeid, name) for name in group_names]
649 if this_group != old_group:
650 l = []
651 for name in group_names:
652 prop = properties[name]
653 if isinstance(prop, hyperdb.Link):
654 group_cl = db.classes[prop.classname]
655 key = group_cl.getkey()
656 value = cl.get(nodeid, name)
657 if value is None:
658 l.append('[unselected %s]'%prop.classname)
659 else:
660 l.append(group_cl.get(cl.get(nodeid, name), key))
661 elif isinstance(prop, hyperdb.Multilink):
662 group_cl = db.classes[prop.classname]
663 key = group_cl.getkey()
664 for value in cl.get(nodeid, name):
665 l.append(group_cl.get(value, key))
666 else:
667 value = cl.get(nodeid, name)
668 if value is None:
669 value = '[empty %s]'%name
670 else:
671 value = str(value)
672 l.append(value)
673 w('<tr class="section-bar">'
674 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
675 len(columns), ', '.join(l)))
676 old_group = this_group
678 # display this node's row
679 for value in globals.values():
680 if hasattr(value, 'nodeid'):
681 value.nodeid = nodeid
682 replace = IndexTemplateReplace(globals, locals(), columns)
683 w(replace.go(template))
685 w('</table>')
688 #
689 # ITEM TEMPLATES
690 #
691 class ItemTemplateReplace:
692 def __init__(self, globals, locals, cl, nodeid):
693 self.globals = globals
694 self.locals = locals
695 self.cl = cl
696 self.nodeid = nodeid
698 def go(self, text, replace=re.compile(
699 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
700 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
701 return replace.sub(self, text)
703 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
704 if m.group('name'):
705 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
706 replace = ItemTemplateReplace(self.globals, {}, self.cl,
707 self.nodeid)
708 return replace.go(m.group('text'))
709 else:
710 return ''
711 if m.group('display'):
712 command = m.group('command')
713 return eval(command, self.globals, self.locals)
714 print '*** unhandled match', m.groupdict()
716 def item(client, templates, db, classname, nodeid, replace=re.compile(
717 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
718 r'(?P<endprop></property>)|'
719 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
721 globals = {
722 'plain': Plain(db, templates, classname, nodeid),
723 'field': Field(db, templates, classname, nodeid),
724 'menu': Menu(db, templates, classname, nodeid),
725 'link': Link(db, templates, classname, nodeid),
726 'count': Count(db, templates, classname, nodeid),
727 'reldate': Reldate(db, templates, classname, nodeid),
728 'download': Download(db, templates, classname, nodeid),
729 'checklist': Checklist(db, templates, classname, nodeid),
730 'list': List(db, templates, classname, nodeid),
731 'history': History(db, templates, classname, nodeid),
732 'submit': Submit(db, templates, classname, nodeid),
733 'note': Note(db, templates, classname, nodeid)
734 }
736 cl = db.classes[classname]
737 properties = cl.getprops()
739 if properties.has_key('type') and properties.has_key('content'):
740 pass
741 # XXX we really want to return this as a downloadable...
742 # currently I handle this at a higher level by detecting 'file'
743 # designators...
745 w = client.write
746 w('<form action="%s%s">'%(classname, nodeid))
747 s = open(os.path.join(templates, classname+'.item')).read()
748 replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
749 w(replace.go(s))
750 w('</form>')
753 def newitem(client, templates, db, classname, form, replace=re.compile(
754 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
755 r'(?P<endprop></property>)|'
756 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
757 globals = {
758 'plain': Plain(db, templates, classname, form=form),
759 'field': Field(db, templates, classname, form=form),
760 'menu': Menu(db, templates, classname, form=form),
761 'link': Link(db, templates, classname, form=form),
762 'count': Count(db, templates, classname, form=form),
763 'reldate': Reldate(db, templates, classname, form=form),
764 'download': Download(db, templates, classname, form=form),
765 'checklist': Checklist(db, templates, classname, form=form),
766 'list': List(db, templates, classname, form=form),
767 'history': History(db, templates, classname, form=form),
768 'submit': Submit(db, templates, classname, form=form),
769 'note': Note(db, templates, classname, form=form)
770 }
772 cl = db.classes[classname]
773 properties = cl.getprops()
775 w = client.write
776 try:
777 s = open(os.path.join(templates, classname+'.newitem')).read()
778 except:
779 s = open(os.path.join(templates, classname+'.item')).read()
780 w('<form action="new%s" method="POST" enctype="multipart/form-data">'%classname)
781 for key in form.keys():
782 if key[0] == ':':
783 value = form[key].value
784 if type(value) != type([]): value = [value]
785 for value in value:
786 w('<input type="hidden" name="%s" value="%s">'%(key, value))
787 replace = ItemTemplateReplace(globals, locals(), None, None)
788 w(replace.go(s))
789 w('</form>')
791 #
792 # $Log: not supported by cvs2svn $
793 # Revision 1.28 2001/10/21 00:00:16 richard
794 # Fixed Checklist function - wasn't always working on a list.
795 #
796 # Revision 1.27 2001/10/20 12:13:44 richard
797 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
798 #
799 # Revision 1.26 2001/10/14 10:55:00 richard
800 # Handle empty strings in HTML template Link function
801 #
802 # Revision 1.25 2001/10/09 07:25:59 richard
803 # Added the Password property type. See "pydoc roundup.password" for
804 # implementation details. Have updated some of the documentation too.
805 #
806 # Revision 1.24 2001/09/27 06:45:58 richard
807 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
808 # on the plain() template function to escape the text for HTML.
809 #
810 # Revision 1.23 2001/09/10 09:47:18 richard
811 # Fixed bug in the generation of links to Link/Multilink in indexes.
812 # (thanks Hubert Hoegl)
813 # Added AssignedTo to the "classic" schema's item page.
814 #
815 # Revision 1.22 2001/08/30 06:01:17 richard
816 # Fixed missing import in mailgw :(
817 #
818 # Revision 1.21 2001/08/16 07:34:59 richard
819 # better CGI text searching - but hidden filter fields are disappearing...
820 #
821 # Revision 1.20 2001/08/15 23:43:18 richard
822 # Fixed some isFooTypes that I missed.
823 # Refactored some code in the CGI code.
824 #
825 # Revision 1.19 2001/08/12 06:32:36 richard
826 # using isinstance(blah, Foo) now instead of isFooType
827 #
828 # Revision 1.18 2001/08/07 00:24:42 richard
829 # stupid typo
830 #
831 # Revision 1.17 2001/08/07 00:15:51 richard
832 # Added the copyright/license notice to (nearly) all files at request of
833 # Bizar Software.
834 #
835 # Revision 1.16 2001/08/01 03:52:23 richard
836 # Checklist was using wrong name.
837 #
838 # Revision 1.15 2001/07/30 08:12:17 richard
839 # Added time logging and file uploading to the templates.
840 #
841 # Revision 1.14 2001/07/30 06:17:45 richard
842 # Features:
843 # . Added ability for cgi newblah forms to indicate that the new node
844 # should be linked somewhere.
845 # Fixed:
846 # . Fixed the agument handling for the roundup-admin find command.
847 # . Fixed handling of summary when no note supplied for newblah. Again.
848 # . Fixed detection of no form in htmltemplate Field display.
849 #
850 # Revision 1.13 2001/07/30 02:37:53 richard
851 # Temporary measure until we have decent schema migration.
852 #
853 # Revision 1.12 2001/07/30 01:24:33 richard
854 # Handles new node display now.
855 #
856 # Revision 1.11 2001/07/29 09:31:35 richard
857 # oops
858 #
859 # Revision 1.10 2001/07/29 09:28:23 richard
860 # Fixed sorting by clicking on column headings.
861 #
862 # Revision 1.9 2001/07/29 08:27:40 richard
863 # Fixed handling of passed-in values in form elements (ie. during a
864 # drill-down)
865 #
866 # Revision 1.8 2001/07/29 07:01:39 richard
867 # Added vim command to all source so that we don't get no steenkin' tabs :)
868 #
869 # Revision 1.7 2001/07/29 05:36:14 richard
870 # Cleanup of the link label generation.
871 #
872 # Revision 1.6 2001/07/29 04:06:42 richard
873 # Fixed problem in link display when Link value is None.
874 #
875 # Revision 1.5 2001/07/28 08:17:09 richard
876 # fixed use of stylesheet
877 #
878 # Revision 1.4 2001/07/28 07:59:53 richard
879 # Replaced errno integers with their module values.
880 # De-tabbed templatebuilder.py
881 #
882 # Revision 1.3 2001/07/25 03:39:47 richard
883 # Hrm - displaying links to classes that don't specify a key property. I've
884 # got it defaulting to 'name', then 'title' and then a "random" property (first
885 # one returned by getprops().keys().
886 # Needs to be moved onto the Class I think...
887 #
888 # Revision 1.2 2001/07/22 12:09:32 richard
889 # Final commit of Grande Splite
890 #
891 # Revision 1.1 2001/07/22 11:58:35 richard
892 # More Grande Splite
893 #
894 #
895 # vim: set filetype=python ts=4 sw=4 et si