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.40 2001-11-03 01:56:51 richard Exp $
20 import os, re, StringIO, urllib, cgi, errno
22 import hyperdb, date, password
24 # This imports the StructureText functionality for the do_stext function
25 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
26 try:
27 from StructuredText.StructuredText import HTML as StructuredText
28 except ImportError:
29 StructuredText = None
31 class TemplateFunctions:
32 def __init__(self):
33 self.form = None
34 self.nodeid = None
35 self.filterspec = None
36 self.globals = {}
37 for key in TemplateFunctions.__dict__.keys():
38 if key[:3] == 'do_':
39 self.globals[key[3:]] = getattr(self, key)
41 def do_plain(self, property, escape=0):
42 ''' display a String property directly;
44 display a Date property in a specified time zone with an option to
45 omit the time from the date stamp;
47 for a Link or Multilink property, display the key strings of the
48 linked nodes (or the ids if the linked class has no key property)
49 '''
50 if not self.nodeid and self.form is None:
51 return '[Field: not called from item]'
52 propclass = self.properties[property]
53 if self.nodeid:
54 value = self.cl.get(self.nodeid, property)
55 else:
56 # TODO: pull the value from the form
57 if isinstance(propclass, hyperdb.Multilink): value = []
58 else: value = ''
59 if isinstance(propclass, hyperdb.String):
60 if value is None: value = ''
61 else: value = str(value)
62 elif isinstance(propclass, hyperdb.Password):
63 if value is None: value = ''
64 else: value = '*encrypted*'
65 elif isinstance(propclass, hyperdb.Date):
66 value = str(value)
67 elif isinstance(propclass, hyperdb.Interval):
68 value = str(value)
69 elif isinstance(propclass, hyperdb.Link):
70 linkcl = self.db.classes[propclass.classname]
71 k = linkcl.labelprop()
72 if value: value = str(linkcl.get(value, k))
73 else: value = '[unselected]'
74 elif isinstance(propclass, hyperdb.Multilink):
75 linkcl = self.db.classes[propclass.classname]
76 k = linkcl.labelprop()
77 value = ', '.join([linkcl.get(i, k) for i in value])
78 else:
79 s = 'Plain: bad propclass "%s"'%propclass
80 if escape:
81 value = cgi.escape(value)
82 return value
84 def do_stext(self, property, escape=0):
85 '''Render as structured text using the StructuredText module
86 (see above for details)
87 '''
88 s = self.do_plain(property, escape=escape)
89 if not StructuredText:
90 return s
91 return StructuredText(s,level=1,header=0)
93 def do_field(self, property, size=None, height=None, showid=0):
94 ''' display a property like the plain displayer, but in a text field
95 to be edited
96 '''
97 if not self.nodeid and self.form is None and self.filterspec is None:
98 return '[Field: not called from item]'
99 propclass = self.properties[property]
100 if self.nodeid:
101 value = self.cl.get(self.nodeid, property, None)
102 # TODO: remove this from the code ... it's only here for
103 # handling schema changes, and they should be handled outside
104 # of this code...
105 if isinstance(propclass, hyperdb.Multilink) and value is None:
106 value = []
107 elif self.filterspec is not None:
108 if isinstance(propclass, hyperdb.Multilink):
109 value = self.filterspec.get(property, [])
110 else:
111 value = self.filterspec.get(property, '')
112 else:
113 # TODO: pull the value from the form
114 if isinstance(propclass, hyperdb.Multilink): value = []
115 else: value = ''
116 if (isinstance(propclass, hyperdb.String) or
117 isinstance(propclass, hyperdb.Date) or
118 isinstance(propclass, hyperdb.Interval)):
119 size = size or 30
120 if value is None:
121 value = ''
122 else:
123 value = cgi.escape(value)
124 value = '"'.join(value.split('"'))
125 s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
126 elif isinstance(propclass, hyperdb.Password):
127 size = size or 30
128 s = '<input type="password" name="%s" size="%s">'%(property, size)
129 elif isinstance(propclass, hyperdb.Link):
130 linkcl = self.db.classes[propclass.classname]
131 l = ['<select name="%s">'%property]
132 k = linkcl.labelprop()
133 if value is None:
134 s = 'selected '
135 else:
136 s = ''
137 l.append('<option %svalue="-1">- no selection -</option>'%s)
138 for optionid in linkcl.list():
139 option = linkcl.get(optionid, k)
140 s = ''
141 if optionid == value:
142 s = 'selected '
143 if showid:
144 lab = '%s%s: %s'%(propclass.classname, optionid, option)
145 else:
146 lab = option
147 if size is not None and len(lab) > size:
148 lab = lab[:size-3] + '...'
149 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
150 l.append('</select>')
151 s = '\n'.join(l)
152 elif isinstance(propclass, hyperdb.Multilink):
153 linkcl = self.db.classes[propclass.classname]
154 list = linkcl.list()
155 height = height or min(len(list), 7)
156 l = ['<select multiple name="%s" size="%s">'%(property, height)]
157 k = linkcl.labelprop()
158 for optionid in list:
159 option = linkcl.get(optionid, k)
160 s = ''
161 if optionid in value:
162 s = 'selected '
163 if showid:
164 lab = '%s%s: %s'%(propclass.classname, optionid, option)
165 else:
166 lab = option
167 if size is not None and len(lab) > size:
168 lab = lab[:size-3] + '...'
169 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
170 l.append('</select>')
171 s = '\n'.join(l)
172 else:
173 s = 'Plain: bad propclass "%s"'%propclass
174 return s
176 def do_menu(self, property, size=None, height=None, showid=0):
177 ''' for a Link property, display a menu of the available choices
178 '''
179 propclass = self.properties[property]
180 if self.nodeid:
181 value = self.cl.get(self.nodeid, property)
182 else:
183 # TODO: pull the value from the form
184 if isinstance(propclass, hyperdb.Multilink): value = []
185 else: value = None
186 if isinstance(propclass, hyperdb.Link):
187 linkcl = self.db.classes[propclass.classname]
188 l = ['<select name="%s">'%property]
189 k = linkcl.labelprop()
190 s = ''
191 if value is None:
192 s = 'selected '
193 l.append('<option %svalue="-1">- no selection -</option>'%s)
194 for optionid in linkcl.list():
195 option = linkcl.get(optionid, k)
196 s = ''
197 if optionid == value:
198 s = 'selected '
199 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
200 l.append('</select>')
201 return '\n'.join(l)
202 if isinstance(propclass, hyperdb.Multilink):
203 linkcl = self.db.classes[propclass.classname]
204 list = linkcl.list()
205 height = height or min(len(list), 7)
206 l = ['<select multiple name="%s" size="%s">'%(property, height)]
207 k = linkcl.labelprop()
208 for optionid in list:
209 option = linkcl.get(optionid, k)
210 s = ''
211 if optionid in value:
212 s = 'selected '
213 if showid:
214 lab = '%s%s: %s'%(propclass.classname, optionid, option)
215 else:
216 lab = option
217 if size is not None and len(lab) > size:
218 lab = lab[:size-3] + '...'
219 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
220 l.append('</select>')
221 return '\n'.join(l)
222 return '[Menu: not a link]'
224 #XXX deviates from spec
225 def do_link(self, property=None, is_download=0):
226 '''For a Link or Multilink property, display the names of the linked
227 nodes, hyperlinked to the item views on those nodes.
228 For other properties, link to this node with the property as the
229 text.
231 If is_download is true, append the property value to the generated
232 URL so that the link may be used as a download link and the
233 downloaded file name is correct.
234 '''
235 if not self.nodeid and self.form is None:
236 return '[Link: not called from item]'
237 propclass = self.properties[property]
238 if self.nodeid:
239 value = self.cl.get(self.nodeid, property)
240 else:
241 if isinstance(propclass, hyperdb.Multilink): value = []
242 elif isinstance(propclass, hyperdb.Link): value = None
243 else: value = ''
244 if isinstance(propclass, hyperdb.Link):
245 linkname = propclass.classname
246 if value is None: return '[no %s]'%property.capitalize()
247 linkcl = self.db.classes[linkname]
248 k = linkcl.labelprop()
249 linkvalue = linkcl.get(value, k)
250 if is_download:
251 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
252 linkvalue, linkvalue)
253 else:
254 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
255 if isinstance(propclass, hyperdb.Multilink):
256 linkname = propclass.classname
257 linkcl = self.db.classes[linkname]
258 k = linkcl.labelprop()
259 if not value : return '[no %s]'%property.capitalize()
260 l = []
261 for value in value:
262 linkvalue = linkcl.get(value, k)
263 if is_download:
264 l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
265 linkvalue, linkvalue))
266 else:
267 l.append('<a href="%s%s">%s</a>'%(linkname, value,
268 linkvalue))
269 return ', '.join(l)
270 if isinstance(propclass, hyperdb.String):
271 if value == '': value = '[no %s]'%property.capitalize()
272 if is_download:
273 return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
274 value, value)
275 else:
276 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
278 def do_count(self, property, **args):
279 ''' for a Multilink property, display a count of the number of links in
280 the list
281 '''
282 if not self.nodeid:
283 return '[Count: not called from item]'
284 propclass = self.properties[property]
285 value = self.cl.get(self.nodeid, property)
286 if isinstance(propclass, hyperdb.Multilink):
287 return str(len(value))
288 return '[Count: not a Multilink]'
290 # XXX pretty is definitely new ;)
291 def do_reldate(self, property, pretty=0):
292 ''' display a Date property in terms of an interval relative to the
293 current date (e.g. "+ 3w", "- 2d").
295 with the 'pretty' flag, make it pretty
296 '''
297 if not self.nodeid and self.form is None:
298 return '[Reldate: not called from item]'
299 propclass = self.properties[property]
300 if isinstance(not propclass, hyperdb.Date):
301 return '[Reldate: not a Date]'
302 if self.nodeid:
303 value = self.cl.get(self.nodeid, property)
304 else:
305 value = date.Date('.')
306 interval = value - date.Date('.')
307 if pretty:
308 if not self.nodeid:
309 return 'now'
310 pretty = interval.pretty()
311 if pretty is None:
312 pretty = value.pretty()
313 return pretty
314 return str(interval)
316 def do_download(self, property, **args):
317 ''' show a Link("file") or Multilink("file") property using links that
318 allow you to download files
319 '''
320 if not self.nodeid:
321 return '[Download: not called from item]'
322 propclass = self.properties[property]
323 value = self.cl.get(self.nodeid, property)
324 if isinstance(propclass, hyperdb.Link):
325 linkcl = self.db.classes[propclass.classname]
326 linkvalue = linkcl.get(value, k)
327 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
328 if isinstance(propclass, hyperdb.Multilink):
329 linkcl = self.db.classes[propclass.classname]
330 l = []
331 for value in value:
332 linkvalue = linkcl.get(value, k)
333 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
334 return ', '.join(l)
335 return '[Download: not a link]'
338 def do_checklist(self, property, **args):
339 ''' for a Link or Multilink property, display checkboxes for the
340 available choices to permit filtering
341 '''
342 propclass = self.properties[property]
343 if (not isinstance(propclass, hyperdb.Link) and not
344 isinstance(propclass, hyperdb.Multilink)):
345 return '[Checklist: not a link]'
347 # get our current checkbox state
348 if self.nodeid:
349 # get the info from the node - make sure it's a list
350 if isinstance(propclass, hyperdb.Link):
351 value = [self.cl.get(self.nodeid, property)]
352 else:
353 value = self.cl.get(self.nodeid, property)
354 elif self.filterspec is not None:
355 # get the state from the filter specification (always a list)
356 value = self.filterspec.get(property, [])
357 else:
358 # it's a new node, so there's no state
359 value = []
361 # so we can map to the linked node's "lable" property
362 linkcl = self.db.classes[propclass.classname]
363 l = []
364 k = linkcl.labelprop()
365 for optionid in linkcl.list():
366 option = linkcl.get(optionid, k)
367 if optionid in value or option in value:
368 checked = 'checked'
369 else:
370 checked = ''
371 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
372 option, checked, property, option))
374 # for Links, allow the "unselected" option too
375 if isinstance(propclass, hyperdb.Link):
376 if value is None or '-1' in value:
377 checked = 'checked'
378 else:
379 checked = ''
380 l.append('[unselected]:<input type="checkbox" %s name="%s" '
381 'value="-1">'%(checked, property))
382 return '\n'.join(l)
384 def do_note(self, rows=5, cols=80):
385 ''' display a "note" field, which is a text area for entering a note to
386 go along with a change.
387 '''
388 # TODO: pull the value from the form
389 return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
390 '</textarea>'%(rows, cols)
392 # XXX new function
393 def do_list(self, property, reverse=0):
394 ''' list the items specified by property using the standard index for
395 the class
396 '''
397 propcl = self.properties[property]
398 if not isinstance(propcl, hyperdb.Multilink):
399 return '[List: not a Multilink]'
400 value = self.cl.get(self.nodeid, property)
401 if reverse:
402 value.reverse()
404 # render the sub-index into a string
405 fp = StringIO.StringIO()
406 try:
407 write_save = self.client.write
408 self.client.write = fp.write
409 index = IndexTemplate(self.client, self.templates, propcl.classname)
410 index.render(nodeids=value, show_display_form=0)
411 finally:
412 self.client.write = write_save
414 return fp.getvalue()
416 # XXX new function
417 def do_history(self, **args):
418 ''' list the history of the item
419 '''
420 if self.nodeid is None:
421 return "[History: node doesn't exist]"
423 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
424 '<tr class="list-header">',
425 '<td><span class="list-item"><strong>Date</strong></span></td>',
426 '<td><span class="list-item"><strong>User</strong></span></td>',
427 '<td><span class="list-item"><strong>Action</strong></span></td>',
428 '<td><span class="list-item"><strong>Args</strong></span></td>']
430 for id, date, user, action, args in self.cl.history(self.nodeid):
431 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
432 date, user, action, args))
433 l.append('</table>')
434 return '\n'.join(l)
436 # XXX new function
437 def do_submit(self):
438 ''' add a submit button for the item
439 '''
440 if self.nodeid:
441 return '<input type="submit" value="Submit Changes">'
442 elif self.form is not None:
443 return '<input type="submit" value="Submit New Entry">'
444 else:
445 return '[Submit: not called from item]'
448 #
449 # INDEX TEMPLATES
450 #
451 class IndexTemplateReplace:
452 def __init__(self, globals, locals, props):
453 self.globals = globals
454 self.locals = locals
455 self.props = props
457 replace=re.compile(
458 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
459 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
460 def go(self, text):
461 return self.replace.sub(self, text)
463 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
464 if m.group('name'):
465 if m.group('name') in self.props:
466 text = m.group('text')
467 replace = IndexTemplateReplace(self.globals, {}, self.props)
468 return replace.go(m.group('text'))
469 else:
470 return ''
471 if m.group('display'):
472 command = m.group('command')
473 return eval(command, self.globals, self.locals)
474 print '*** unhandled match', m.groupdict()
476 class IndexTemplate(TemplateFunctions):
477 def __init__(self, client, templates, classname):
478 self.client = client
479 self.templates = templates
480 self.classname = classname
482 # derived
483 self.db = self.client.db
484 self.cl = self.db.classes[self.classname]
485 self.properties = self.cl.getprops()
487 TemplateFunctions.__init__(self)
489 col_re=re.compile(r'<property\s+name="([^>]+)">')
490 def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
491 show_display_form=1, nodeids=None, show_customization=1):
492 self.filterspec = filterspec
494 w = self.client.write
496 # get the filter template
497 try:
498 filter_template = open(os.path.join(self.templates,
499 self.classname+'.filter')).read()
500 all_filters = self.col_re.findall(filter_template)
501 except IOError, error:
502 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
503 filter_template = None
504 all_filters = []
506 # XXX deviate from spec here ...
507 # load the index section template and figure the default columns from it
508 template = open(os.path.join(self.templates,
509 self.classname+'.index')).read()
510 all_columns = self.col_re.findall(template)
511 if not columns:
512 columns = []
513 for name in all_columns:
514 columns.append(name)
515 else:
516 # re-sort columns to be the same order as all_columns
517 l = []
518 for name in all_columns:
519 if name in columns:
520 l.append(name)
521 columns = l
523 # display the filter section
524 if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
525 self.client.FILTER_POSITION in ('top and bottom', 'top')):
526 w('<form action="index">\n')
527 self.filter_section(filter_template, filter, columns, group,
528 all_filters, all_columns, show_customization)
529 # make sure that the sorting doesn't get lost either
530 if sort:
531 w('<input type="hidden" name=":sort" value="%s">'%
532 ','.join(sort))
533 w('</form>\n')
536 # now display the index section
537 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
538 w('<tr class="list-header">\n')
539 for name in columns:
540 cname = name.capitalize()
541 if show_display_form:
542 sb = self.sortby(name, filterspec, columns, filter, group, sort)
543 anchor = "%s?%s"%(self.classname, sb)
544 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
545 anchor, cname))
546 else:
547 w('<td><span class="list-header">%s</span></td>\n'%cname)
548 w('</tr>\n')
550 # this stuff is used for group headings - optimise the group names
551 old_group = None
552 group_names = []
553 if group:
554 for name in group:
555 if name[0] == '-': group_names.append(name[1:])
556 else: group_names.append(name)
558 # now actually loop through all the nodes we get from the filter and
559 # apply the template
560 if nodeids is None:
561 nodeids = self.cl.filter(filterspec, sort, group)
562 for nodeid in nodeids:
563 # check for a group heading
564 if group_names:
565 this_group = [self.cl.get(nodeid, name) for name in group_names]
566 if this_group != old_group:
567 l = []
568 for name in group_names:
569 prop = self.properties[name]
570 if isinstance(prop, hyperdb.Link):
571 group_cl = self.db.classes[prop.classname]
572 key = group_cl.getkey()
573 value = self.cl.get(nodeid, name)
574 if value is None:
575 l.append('[unselected %s]'%prop.classname)
576 else:
577 l.append(group_cl.get(self.cl.get(nodeid,
578 name), key))
579 elif isinstance(prop, hyperdb.Multilink):
580 group_cl = self.db.classes[prop.classname]
581 key = group_cl.getkey()
582 for value in self.cl.get(nodeid, name):
583 l.append(group_cl.get(value, key))
584 else:
585 value = self.cl.get(nodeid, name)
586 if value is None:
587 value = '[empty %s]'%name
588 else:
589 value = str(value)
590 l.append(value)
591 w('<tr class="section-bar">'
592 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
593 len(columns), ', '.join(l)))
594 old_group = this_group
596 # display this node's row
597 replace = IndexTemplateReplace(self.globals, locals(), columns)
598 self.nodeid = nodeid
599 w(replace.go(template))
600 self.nodeid = None
602 w('</table>')
604 # display the filter section
605 if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
606 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
607 w('<form action="index">\n')
608 self.filter_section(filter_template, filter, columns, group,
609 all_filters, all_columns, show_customization)
610 # make sure that the sorting doesn't get lost either
611 if sort:
612 w('<input type="hidden" name=":sort" value="%s">'%
613 ','.join(sort))
614 w('</form>\n')
617 def filter_section(self, template, filter, columns, group, all_filters,
618 all_columns, show_customization):
620 w = self.client.write
622 if template and filter:
623 # display the filter section
624 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
625 w('<tr class="location-bar">')
626 w(' <th align="left" colspan="2">Filter specification...</th>')
627 w('</tr>')
628 replace = IndexTemplateReplace(self.globals, locals(), filter)
629 w(replace.go(template))
630 w('<tr class="location-bar"><td width="1%%"> </td>')
631 w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
632 w('</table>')
634 # now add in the filter/columns/group/etc config table form
635 w('<input type="hidden" name="show_customization" value="%s">' %
636 show_customization )
637 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
638 names = []
639 for name in self.properties.keys():
640 if name in all_filters or name in all_columns:
641 names.append(name)
642 if show_customization:
643 action = '-'
644 else:
645 action = '+'
646 # hide the values for filters, columns and grouping in the form
647 # if the customization widget is not visible
648 for name in names:
649 if all_filters and name in filter:
650 w('<input type="hidden" name=":filter" value="%s">' % name)
651 if all_columns and name in columns:
652 w('<input type="hidden" name=":columns" value="%s">' % name)
653 if all_columns and name in group:
654 w('<input type="hidden" name=":group" value="%s">' % name)
656 # TODO: The widget style can go into the stylesheet
657 w('<th align="left" colspan=%s>'
658 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
659 'customisation...</th></tr>\n'%(len(names)+1, action))
661 if not show_customization:
662 w('</table>\n')
664 w('<tr class="location-bar"><th> </th>')
665 for name in names:
666 w('<th>%s</th>'%name.capitalize())
667 w('</tr>\n')
669 # Filter
670 if all_filters:
671 w('<tr><th width="1%" align=right class="location-bar">'
672 'Filters</th>\n')
673 for name in names:
674 if name not in all_filters:
675 w('<td> </td>')
676 continue
677 if name in filter: checked=' checked'
678 else: checked=''
679 w('<td align=middle>\n')
680 w(' <input type="checkbox" name=":filter" value="%s" '
681 '%s></td>\n'%(name, checked))
682 w('</tr>\n')
684 # Columns
685 if all_columns:
686 w('<tr><th width="1%" align=right class="location-bar">'
687 'Columns</th>\n')
688 for name in names:
689 if name not in all_columns:
690 w('<td> </td>')
691 continue
692 if name in columns: checked=' checked'
693 else: checked=''
694 w('<td align=middle>\n')
695 w(' <input type="checkbox" name=":columns" value="%s"'
696 '%s></td>\n'%(name, checked))
697 w('</tr>\n')
699 # Grouping
700 w('<tr><th width="1%" align=right class="location-bar">'
701 'Grouping</th>\n')
702 for name in names:
703 prop = self.properties[name]
704 if name not in all_columns:
705 w('<td> </td>')
706 continue
707 if name in group: checked=' checked'
708 else: checked=''
709 w('<td align=middle>\n')
710 w(' <input type="checkbox" name=":group" value="%s"'
711 '%s></td>\n'%(name, checked))
712 w('</tr>\n')
714 w('<tr class="location-bar"><td width="1%"> </td>')
715 w('<td colspan="%s">'%len(names))
716 w('<input type="submit" name="action" value="Redisplay"></td>')
717 w('</tr>\n')
718 w('</table>\n')
721 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
722 l = []
723 w = l.append
724 for k, v in filterspec.items():
725 k = urllib.quote(k)
726 if type(v) == type([]):
727 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
728 else:
729 w('%s=%s'%(k, urllib.quote(v)))
730 if columns:
731 w(':columns=%s'%','.join(map(urllib.quote, columns)))
732 if filter:
733 w(':filter=%s'%','.join(map(urllib.quote, filter)))
734 if group:
735 w(':group=%s'%','.join(map(urllib.quote, group)))
736 m = []
737 s_dir = ''
738 for name in sort:
739 dir = name[0]
740 if dir == '-':
741 name = name[1:]
742 else:
743 dir = ''
744 if sort_name == name:
745 if dir == '-':
746 s_dir = ''
747 else:
748 s_dir = '-'
749 else:
750 m.append(dir+urllib.quote(name))
751 m.insert(0, s_dir+urllib.quote(sort_name))
752 # so things don't get completely out of hand, limit the sort to
753 # two columns
754 w(':sort=%s'%','.join(m[:2]))
755 return '&'.join(l)
757 #
758 # ITEM TEMPLATES
759 #
760 class ItemTemplateReplace:
761 def __init__(self, globals, locals, cl, nodeid):
762 self.globals = globals
763 self.locals = locals
764 self.cl = cl
765 self.nodeid = nodeid
767 replace=re.compile(
768 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
769 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
770 def go(self, text):
771 return self.replace.sub(self, text)
773 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
774 if m.group('name'):
775 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
776 replace = ItemTemplateReplace(self.globals, {}, self.cl,
777 self.nodeid)
778 return replace.go(m.group('text'))
779 else:
780 return ''
781 if m.group('display'):
782 command = m.group('command')
783 return eval(command, self.globals, self.locals)
784 print '*** unhandled match', m.groupdict()
787 class ItemTemplate(TemplateFunctions):
788 def __init__(self, client, templates, classname):
789 self.client = client
790 self.templates = templates
791 self.classname = classname
793 # derived
794 self.db = self.client.db
795 self.cl = self.db.classes[self.classname]
796 self.properties = self.cl.getprops()
798 TemplateFunctions.__init__(self)
800 def render(self, nodeid):
801 self.nodeid = nodeid
803 if (self.properties.has_key('type') and
804 self.properties.has_key('content')):
805 pass
806 # XXX we really want to return this as a downloadable...
807 # currently I handle this at a higher level by detecting 'file'
808 # designators...
810 w = self.client.write
811 w('<form action="%s%s">'%(self.classname, nodeid))
812 s = open(os.path.join(self.templates, self.classname+'.item')).read()
813 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
814 w(replace.go(s))
815 w('</form>')
818 class NewItemTemplate(TemplateFunctions):
819 def __init__(self, client, templates, classname):
820 self.client = client
821 self.templates = templates
822 self.classname = classname
824 # derived
825 self.db = self.client.db
826 self.cl = self.db.classes[self.classname]
827 self.properties = self.cl.getprops()
829 TemplateFunctions.__init__(self)
831 def render(self, form):
832 self.form = form
833 w = self.client.write
834 c = self.classname
835 try:
836 s = open(os.path.join(self.templates, c+'.newitem')).read()
837 except:
838 s = open(os.path.join(self.templates, c+'.item')).read()
839 w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
840 for key in form.keys():
841 if key[0] == ':':
842 value = form[key].value
843 if type(value) != type([]): value = [value]
844 for value in value:
845 w('<input type="hidden" name="%s" value="%s">'%(key, value))
846 replace = ItemTemplateReplace(self.globals, locals(), None, None)
847 w(replace.go(s))
848 w('</form>')
850 #
851 # $Log: not supported by cvs2svn $
852 # Revision 1.39 2001/11/03 01:43:47 richard
853 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
854 #
855 # Revision 1.38 2001/10/31 06:58:51 richard
856 # Added the wrap="hard" attribute to the textarea of the note field so the
857 # messages wrap sanely.
858 #
859 # Revision 1.37 2001/10/31 06:24:35 richard
860 # Added do_stext to htmltemplate, thanks Brad Clements.
861 #
862 # Revision 1.36 2001/10/28 22:51:38 richard
863 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
864 #
865 # Revision 1.35 2001/10/24 00:04:41 richard
866 # Removed the "infinite authentication loop", thanks Roch'e
867 #
868 # Revision 1.34 2001/10/23 22:56:36 richard
869 # Bugfix in filter "widget" placement, thanks Roch'e
870 #
871 # Revision 1.33 2001/10/23 01:00:18 richard
872 # Re-enabled login and registration access after lopping them off via
873 # disabling access for anonymous users.
874 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
875 # a couple of bugs while I was there. Probably introduced a couple, but
876 # things seem to work OK at the moment.
877 #
878 # Revision 1.32 2001/10/22 03:25:01 richard
879 # Added configuration for:
880 # . anonymous user access and registration (deny/allow)
881 # . filter "widget" location on index page (top, bottom, both)
882 # Updated some documentation.
883 #
884 # Revision 1.31 2001/10/21 07:26:35 richard
885 # feature #473127: Filenames. I modified the file.index and htmltemplate
886 # source so that the filename is used in the link and the creation
887 # information is displayed.
888 #
889 # Revision 1.30 2001/10/21 04:44:50 richard
890 # bug #473124: UI inconsistency with Link fields.
891 # This also prompted me to fix a fairly long-standing usability issue -
892 # that of being able to turn off certain filters.
893 #
894 # Revision 1.29 2001/10/21 00:17:56 richard
895 # CGI interface view customisation section may now be hidden (patch from
896 # Roch'e Compaan.)
897 #
898 # Revision 1.28 2001/10/21 00:00:16 richard
899 # Fixed Checklist function - wasn't always working on a list.
900 #
901 # Revision 1.27 2001/10/20 12:13:44 richard
902 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
903 #
904 # Revision 1.26 2001/10/14 10:55:00 richard
905 # Handle empty strings in HTML template Link function
906 #
907 # Revision 1.25 2001/10/09 07:25:59 richard
908 # Added the Password property type. See "pydoc roundup.password" for
909 # implementation details. Have updated some of the documentation too.
910 #
911 # Revision 1.24 2001/09/27 06:45:58 richard
912 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
913 # on the plain() template function to escape the text for HTML.
914 #
915 # Revision 1.23 2001/09/10 09:47:18 richard
916 # Fixed bug in the generation of links to Link/Multilink in indexes.
917 # (thanks Hubert Hoegl)
918 # Added AssignedTo to the "classic" schema's item page.
919 #
920 # Revision 1.22 2001/08/30 06:01:17 richard
921 # Fixed missing import in mailgw :(
922 #
923 # Revision 1.21 2001/08/16 07:34:59 richard
924 # better CGI text searching - but hidden filter fields are disappearing...
925 #
926 # Revision 1.20 2001/08/15 23:43:18 richard
927 # Fixed some isFooTypes that I missed.
928 # Refactored some code in the CGI code.
929 #
930 # Revision 1.19 2001/08/12 06:32:36 richard
931 # using isinstance(blah, Foo) now instead of isFooType
932 #
933 # Revision 1.18 2001/08/07 00:24:42 richard
934 # stupid typo
935 #
936 # Revision 1.17 2001/08/07 00:15:51 richard
937 # Added the copyright/license notice to (nearly) all files at request of
938 # Bizar Software.
939 #
940 # Revision 1.16 2001/08/01 03:52:23 richard
941 # Checklist was using wrong name.
942 #
943 # Revision 1.15 2001/07/30 08:12:17 richard
944 # Added time logging and file uploading to the templates.
945 #
946 # Revision 1.14 2001/07/30 06:17:45 richard
947 # Features:
948 # . Added ability for cgi newblah forms to indicate that the new node
949 # should be linked somewhere.
950 # Fixed:
951 # . Fixed the agument handling for the roundup-admin find command.
952 # . Fixed handling of summary when no note supplied for newblah. Again.
953 # . Fixed detection of no form in htmltemplate Field display.
954 #
955 # Revision 1.13 2001/07/30 02:37:53 richard
956 # Temporary measure until we have decent schema migration.
957 #
958 # Revision 1.12 2001/07/30 01:24:33 richard
959 # Handles new node display now.
960 #
961 # Revision 1.11 2001/07/29 09:31:35 richard
962 # oops
963 #
964 # Revision 1.10 2001/07/29 09:28:23 richard
965 # Fixed sorting by clicking on column headings.
966 #
967 # Revision 1.9 2001/07/29 08:27:40 richard
968 # Fixed handling of passed-in values in form elements (ie. during a
969 # drill-down)
970 #
971 # Revision 1.8 2001/07/29 07:01:39 richard
972 # Added vim command to all source so that we don't get no steenkin' tabs :)
973 #
974 # Revision 1.7 2001/07/29 05:36:14 richard
975 # Cleanup of the link label generation.
976 #
977 # Revision 1.6 2001/07/29 04:06:42 richard
978 # Fixed problem in link display when Link value is None.
979 #
980 # Revision 1.5 2001/07/28 08:17:09 richard
981 # fixed use of stylesheet
982 #
983 # Revision 1.4 2001/07/28 07:59:53 richard
984 # Replaced errno integers with their module values.
985 # De-tabbed templatebuilder.py
986 #
987 # Revision 1.3 2001/07/25 03:39:47 richard
988 # Hrm - displaying links to classes that don't specify a key property. I've
989 # got it defaulting to 'name', then 'title' and then a "random" property (first
990 # one returned by getprops().keys().
991 # Needs to be moved onto the Class I think...
992 #
993 # Revision 1.2 2001/07/22 12:09:32 richard
994 # Final commit of Grande Splite
995 #
996 # Revision 1.1 2001/07/22 11:58:35 richard
997 # More Grande Splite
998 #
999 #
1000 # vim: set filetype=python ts=4 sw=4 et si