0a78595a5784b5efe7eb4673e7c84b0c8fd7d339
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.32 2001-10-22 03:25:01 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 if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'top'):
570 filter_section(w, cl, filter, columns, group, all_filters, all_columns,
571 show_display_form, show_customization)
573 # now display the index section
574 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
575 w('<tr class="list-header">\n')
576 for name in columns:
577 cname = name.capitalize()
578 if show_display_form:
579 anchor = "%s?%s"%(classname, sortby(name, columns, filter,
580 sort, group, filterspec))
581 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
582 anchor, cname))
583 else:
584 w('<td><span class="list-header">%s</span></td>\n'%cname)
585 w('</tr>\n')
587 # this stuff is used for group headings - optimise the group names
588 old_group = None
589 group_names = []
590 if group:
591 for name in group:
592 if name[0] == '-': group_names.append(name[1:])
593 else: group_names.append(name)
595 # now actually loop through all the nodes we get from the filter and
596 # apply the template
597 if nodeids is None:
598 nodeids = cl.filter(filterspec, sort, group)
599 for nodeid in nodeids:
600 # check for a group heading
601 if group_names:
602 this_group = [cl.get(nodeid, name) for name in group_names]
603 if this_group != old_group:
604 l = []
605 for name in group_names:
606 prop = properties[name]
607 if isinstance(prop, hyperdb.Link):
608 group_cl = db.classes[prop.classname]
609 key = group_cl.getkey()
610 value = cl.get(nodeid, name)
611 if value is None:
612 l.append('[unselected %s]'%prop.classname)
613 else:
614 l.append(group_cl.get(cl.get(nodeid, name), key))
615 elif isinstance(prop, hyperdb.Multilink):
616 group_cl = db.classes[prop.classname]
617 key = group_cl.getkey()
618 for value in cl.get(nodeid, name):
619 l.append(group_cl.get(value, key))
620 else:
621 value = cl.get(nodeid, name)
622 if value is None:
623 value = '[empty %s]'%name
624 else:
625 value = str(value)
626 l.append(value)
627 w('<tr class="section-bar">'
628 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
629 len(columns), ', '.join(l)))
630 old_group = this_group
632 # display this node's row
633 for value in globals.values():
634 if hasattr(value, 'nodeid'):
635 value.nodeid = nodeid
636 replace = IndexTemplateReplace(globals, locals(), columns)
637 w(replace.go(template))
639 w('</table>')
641 # display the filter section
642 if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'bottom'):
643 filter_section(w, cl, filter, columns, group, all_filters, all_columns,
644 show_display_form, show_customization)
647 def filter_section(w, cl, filter, columns, group, all_filters, all_columns,
648 show_display_form, show_customization):
649 # now add in the filter/columns/group/etc config table form
650 w('<input type="hidden" name="show_customization" value="%s">' %
651 show_customization )
652 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
653 names = []
654 properties = cl.getprops()
655 for name in properties.keys():
656 if name in all_filters or name in all_columns:
657 names.append(name)
658 w('<tr class="location-bar">')
659 if show_customization:
660 action = '-'
661 else:
662 action = '+'
663 # hide the values for filters, columns and grouping in the form
664 # if the customization widget is not visible
665 for name in names:
666 if all_filters and name in filter:
667 w('<input type="hidden" name=":filter" value="%s">' % name)
668 if all_columns and name in columns:
669 w('<input type="hidden" name=":columns" value="%s">' % name)
670 if all_columns and name in group:
671 w('<input type="hidden" name=":group" value="%s">' % name)
673 if show_display_form:
674 # TODO: The widget style can go into the stylesheet
675 w('<th align="left" colspan=%s>'
676 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
677 'customisation...</th></tr>\n'%(len(names)+1, action))
678 if show_customization:
679 w('<tr class="location-bar"><th> </th>')
680 for name in names:
681 w('<th>%s</th>'%name.capitalize())
682 w('</tr>\n')
684 # Filter
685 if all_filters:
686 w('<tr><th width="1%" align=right class="location-bar">'
687 'Filters</th>\n')
688 for name in names:
689 if name not in all_filters:
690 w('<td> </td>')
691 continue
692 if name in filter: checked=' checked'
693 else: checked=''
694 w('<td align=middle>\n')
695 w(' <input type="checkbox" name=":filter" value="%s" '
696 '%s></td>\n'%(name, checked))
697 w('</tr>\n')
699 # Columns
700 if all_columns:
701 w('<tr><th width="1%" align=right class="location-bar">'
702 'Columns</th>\n')
703 for name in names:
704 if name not in all_columns:
705 w('<td> </td>')
706 continue
707 if name in columns: checked=' checked'
708 else: checked=''
709 w('<td align=middle>\n')
710 w(' <input type="checkbox" name=":columns" value="%s"'
711 '%s></td>\n'%(name, checked))
712 w('</tr>\n')
714 # Grouping
715 w('<tr><th width="1%" align=right class="location-bar">'
716 'Grouping</th>\n')
717 for name in names:
718 prop = properties[name]
719 if name not in all_columns:
720 w('<td> </td>')
721 continue
722 if name in group: checked=' checked'
723 else: checked=''
724 w('<td align=middle>\n')
725 w(' <input type="checkbox" name=":group" value="%s"'
726 '%s></td>\n'%(name, checked))
727 w('</tr>\n')
729 w('<tr class="location-bar"><td width="1%"> </td>')
730 w('<td colspan="%s">'%len(names))
731 w('<input type="submit" name="action" value="Redisplay"></td>')
732 w('</tr>\n')
734 w('</table>\n')
735 w('</form>\n')
737 #
738 # ITEM TEMPLATES
739 #
740 class ItemTemplateReplace:
741 def __init__(self, globals, locals, cl, nodeid):
742 self.globals = globals
743 self.locals = locals
744 self.cl = cl
745 self.nodeid = nodeid
747 def go(self, text, replace=re.compile(
748 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
749 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
750 return replace.sub(self, text)
752 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
753 if m.group('name'):
754 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
755 replace = ItemTemplateReplace(self.globals, {}, self.cl,
756 self.nodeid)
757 return replace.go(m.group('text'))
758 else:
759 return ''
760 if m.group('display'):
761 command = m.group('command')
762 return eval(command, self.globals, self.locals)
763 print '*** unhandled match', m.groupdict()
765 def item(client, templates, db, classname, nodeid, replace=re.compile(
766 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
767 r'(?P<endprop></property>)|'
768 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
770 globals = {
771 'plain': Plain(db, templates, classname, nodeid),
772 'field': Field(db, templates, classname, nodeid),
773 'menu': Menu(db, templates, classname, nodeid),
774 'link': Link(db, templates, classname, nodeid),
775 'count': Count(db, templates, classname, nodeid),
776 'reldate': Reldate(db, templates, classname, nodeid),
777 'download': Download(db, templates, classname, nodeid),
778 'checklist': Checklist(db, templates, classname, nodeid),
779 'list': List(db, templates, classname, nodeid),
780 'history': History(db, templates, classname, nodeid),
781 'submit': Submit(db, templates, classname, nodeid),
782 'note': Note(db, templates, classname, nodeid)
783 }
785 cl = db.classes[classname]
786 properties = cl.getprops()
788 if properties.has_key('type') and properties.has_key('content'):
789 pass
790 # XXX we really want to return this as a downloadable...
791 # currently I handle this at a higher level by detecting 'file'
792 # designators...
794 w = client.write
795 w('<form action="%s%s">'%(classname, nodeid))
796 s = open(os.path.join(templates, classname+'.item')).read()
797 replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
798 w(replace.go(s))
799 w('</form>')
802 def newitem(client, templates, db, classname, form, replace=re.compile(
803 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
804 r'(?P<endprop></property>)|'
805 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
806 globals = {
807 'plain': Plain(db, templates, classname, form=form),
808 'field': Field(db, templates, classname, form=form),
809 'menu': Menu(db, templates, classname, form=form),
810 'link': Link(db, templates, classname, form=form),
811 'count': Count(db, templates, classname, form=form),
812 'reldate': Reldate(db, templates, classname, form=form),
813 'download': Download(db, templates, classname, form=form),
814 'checklist': Checklist(db, templates, classname, form=form),
815 'list': List(db, templates, classname, form=form),
816 'history': History(db, templates, classname, form=form),
817 'submit': Submit(db, templates, classname, form=form),
818 'note': Note(db, templates, classname, form=form)
819 }
821 cl = db.classes[classname]
822 properties = cl.getprops()
824 w = client.write
825 try:
826 s = open(os.path.join(templates, classname+'.newitem')).read()
827 except:
828 s = open(os.path.join(templates, classname+'.item')).read()
829 w('<form action="new%s" method="POST" enctype="multipart/form-data">'%classname)
830 for key in form.keys():
831 if key[0] == ':':
832 value = form[key].value
833 if type(value) != type([]): value = [value]
834 for value in value:
835 w('<input type="hidden" name="%s" value="%s">'%(key, value))
836 replace = ItemTemplateReplace(globals, locals(), None, None)
837 w(replace.go(s))
838 w('</form>')
840 #
841 # $Log: not supported by cvs2svn $
842 # Revision 1.31 2001/10/21 07:26:35 richard
843 # feature #473127: Filenames. I modified the file.index and htmltemplate
844 # source so that the filename is used in the link and the creation
845 # information is displayed.
846 #
847 # Revision 1.30 2001/10/21 04:44:50 richard
848 # bug #473124: UI inconsistency with Link fields.
849 # This also prompted me to fix a fairly long-standing usability issue -
850 # that of being able to turn off certain filters.
851 #
852 # Revision 1.29 2001/10/21 00:17:56 richard
853 # CGI interface view customisation section may now be hidden (patch from
854 # Roch'e Compaan.)
855 #
856 # Revision 1.28 2001/10/21 00:00:16 richard
857 # Fixed Checklist function - wasn't always working on a list.
858 #
859 # Revision 1.27 2001/10/20 12:13:44 richard
860 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
861 #
862 # Revision 1.26 2001/10/14 10:55:00 richard
863 # Handle empty strings in HTML template Link function
864 #
865 # Revision 1.25 2001/10/09 07:25:59 richard
866 # Added the Password property type. See "pydoc roundup.password" for
867 # implementation details. Have updated some of the documentation too.
868 #
869 # Revision 1.24 2001/09/27 06:45:58 richard
870 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
871 # on the plain() template function to escape the text for HTML.
872 #
873 # Revision 1.23 2001/09/10 09:47:18 richard
874 # Fixed bug in the generation of links to Link/Multilink in indexes.
875 # (thanks Hubert Hoegl)
876 # Added AssignedTo to the "classic" schema's item page.
877 #
878 # Revision 1.22 2001/08/30 06:01:17 richard
879 # Fixed missing import in mailgw :(
880 #
881 # Revision 1.21 2001/08/16 07:34:59 richard
882 # better CGI text searching - but hidden filter fields are disappearing...
883 #
884 # Revision 1.20 2001/08/15 23:43:18 richard
885 # Fixed some isFooTypes that I missed.
886 # Refactored some code in the CGI code.
887 #
888 # Revision 1.19 2001/08/12 06:32:36 richard
889 # using isinstance(blah, Foo) now instead of isFooType
890 #
891 # Revision 1.18 2001/08/07 00:24:42 richard
892 # stupid typo
893 #
894 # Revision 1.17 2001/08/07 00:15:51 richard
895 # Added the copyright/license notice to (nearly) all files at request of
896 # Bizar Software.
897 #
898 # Revision 1.16 2001/08/01 03:52:23 richard
899 # Checklist was using wrong name.
900 #
901 # Revision 1.15 2001/07/30 08:12:17 richard
902 # Added time logging and file uploading to the templates.
903 #
904 # Revision 1.14 2001/07/30 06:17:45 richard
905 # Features:
906 # . Added ability for cgi newblah forms to indicate that the new node
907 # should be linked somewhere.
908 # Fixed:
909 # . Fixed the agument handling for the roundup-admin find command.
910 # . Fixed handling of summary when no note supplied for newblah. Again.
911 # . Fixed detection of no form in htmltemplate Field display.
912 #
913 # Revision 1.13 2001/07/30 02:37:53 richard
914 # Temporary measure until we have decent schema migration.
915 #
916 # Revision 1.12 2001/07/30 01:24:33 richard
917 # Handles new node display now.
918 #
919 # Revision 1.11 2001/07/29 09:31:35 richard
920 # oops
921 #
922 # Revision 1.10 2001/07/29 09:28:23 richard
923 # Fixed sorting by clicking on column headings.
924 #
925 # Revision 1.9 2001/07/29 08:27:40 richard
926 # Fixed handling of passed-in values in form elements (ie. during a
927 # drill-down)
928 #
929 # Revision 1.8 2001/07/29 07:01:39 richard
930 # Added vim command to all source so that we don't get no steenkin' tabs :)
931 #
932 # Revision 1.7 2001/07/29 05:36:14 richard
933 # Cleanup of the link label generation.
934 #
935 # Revision 1.6 2001/07/29 04:06:42 richard
936 # Fixed problem in link display when Link value is None.
937 #
938 # Revision 1.5 2001/07/28 08:17:09 richard
939 # fixed use of stylesheet
940 #
941 # Revision 1.4 2001/07/28 07:59:53 richard
942 # Replaced errno integers with their module values.
943 # De-tabbed templatebuilder.py
944 #
945 # Revision 1.3 2001/07/25 03:39:47 richard
946 # Hrm - displaying links to classes that don't specify a key property. I've
947 # got it defaulting to 'name', then 'title' and then a "random" property (first
948 # one returned by getprops().keys().
949 # Needs to be moved onto the Class I think...
950 #
951 # Revision 1.2 2001/07/22 12:09:32 richard
952 # Final commit of Grande Splite
953 #
954 # Revision 1.1 2001/07/22 11:58:35 richard
955 # More Grande Splite
956 #
957 #
958 # vim: set filetype=python ts=4 sw=4 et si