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