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