index c65ef4dea6763cd0ad5db16f6df981b888585938..2f6557a1fdb0abba2b3e4d660048c9cfdc2e923b 100644 (file)
--- a/roundup/htmltemplate.py
+++ b/roundup/htmltemplate.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: htmltemplate.py,v 1.29 2001-10-21 00:17:56 richard Exp $
+# $Id: htmltemplate.py,v 1.89 2002-05-15 06:34:47 richard Exp $
-import os, re, StringIO, urllib, cgi, errno
+__doc__ = """
+Template engine.
+"""
-import hyperdb, date, password
+import os, re, StringIO, urllib, cgi, errno, types, urllib
-class Base:
- def __init__(self, db, templates, classname, nodeid=None, form=None,
- filterspec=None):
- # TODO: really not happy with the way templates is passed on here
- self.db, self.templates = db, templates
- self.classname, self.nodeid = classname, nodeid
- self.form, self.filterspec = form, filterspec
- self.cl = self.db.classes[self.classname]
- self.properties = self.cl.getprops()
+import hyperdb, date
+from i18n import _
-class Plain(Base):
- ''' display a String property directly;
+# This imports the StructureText functionality for the do_stext function
+# get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
+try:
+ from StructuredText.StructuredText import HTML as StructuredText
+except ImportError:
+ StructuredText = None
- display a Date property in a specified time zone with an option to
- omit the time from the date stamp;
+class MissingTemplateError(ValueError):
+ '''Error raised when a template file is missing
+ '''
+ pass
- for a Link or Multilink property, display the key strings of the
- linked nodes (or the ids if the linked class has no key property)
+class TemplateFunctions:
+ '''Defines the templating functions that are used in the HTML templates
+ of the roundup web interface.
'''
- def __call__(self, property, escape=0):
+ def __init__(self):
+ self.form = None
+ self.nodeid = None
+ self.filterspec = None
+ self.globals = {}
+ for key in TemplateFunctions.__dict__.keys():
+ if key[:3] == 'do_':
+ self.globals[key[3:]] = getattr(self, key)
+
+ # These are added by the subclass where appropriate
+ self.client = None
+ self.instance = None
+ self.templates = None
+ self.classname = None
+ self.db = None
+ self.cl = None
+ self.properties = None
+
+ def do_plain(self, property, escape=0):
+ ''' display a String property directly;
+
+ display a Date property in a specified time zone with an option to
+ omit the time from the date stamp;
+
+ for a Link or Multilink property, display the key strings of the
+ linked nodes (or the ids if the linked class has no key property)
+ '''
if not self.nodeid and self.form is None:
- return '[Field: not called from item]'
+ return _('[Field: not called from item]')
propclass = self.properties[property]
if self.nodeid:
- value = self.cl.get(self.nodeid, property)
+ # make sure the property is a valid one
+ # TODO: this tests, but we should handle the exception
+ dummy = self.cl.getprops()[property]
+
+ # get the value for this property
+ try:
+ value = self.cl.get(self.nodeid, property)
+ except KeyError:
+ # a KeyError here means that the node doesn't have a value
+ # for the specified property
+ if isinstance(propclass, hyperdb.Multilink): value = []
+ else: value = ''
else:
# TODO: pull the value from the form
if isinstance(propclass, hyperdb.Multilink): value = []
else: value = str(value)
elif isinstance(propclass, hyperdb.Password):
if value is None: value = ''
- else: value = '*encrypted*'
+ else: value = _('*encrypted*')
elif isinstance(propclass, hyperdb.Date):
+ # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
value = str(value)
elif isinstance(propclass, hyperdb.Interval):
value = str(value)
elif isinstance(propclass, hyperdb.Link):
linkcl = self.db.classes[propclass.classname]
k = linkcl.labelprop()
- if value: value = str(linkcl.get(value, k))
- else: value = '[unselected]'
+ if value:
+ value = linkcl.get(value, k)
+ else:
+ value = _('[unselected]')
elif isinstance(propclass, hyperdb.Multilink):
linkcl = self.db.classes[propclass.classname]
k = linkcl.labelprop()
- value = ', '.join([linkcl.get(i, k) for i in value])
+ labels = []
+ for v in value:
+ labels.append(linkcl.get(v, k))
+ value = ', '.join(labels)
else:
- s = 'Plain: bad propclass "%s"'%propclass
+ value = _('Plain: bad propclass "%(propclass)s"')%locals()
if escape:
- return cgi.escape(value)
+ value = cgi.escape(value)
return value
-class Field(Base):
- ''' display a property like the plain displayer, but in a text field
- to be edited
- '''
- def __call__(self, property, size=None, height=None, showid=0):
- if not self.nodeid and self.form is None and self.filterspec is None:
- return '[Field: not called from item]'
+ def do_stext(self, property, escape=0):
+ '''Render as structured text using the StructuredText module
+ (see above for details)
+ '''
+ s = self.do_plain(property, escape=escape)
+ if not StructuredText:
+ return s
+ return StructuredText(s,level=1,header=0)
+
+ def determine_value(self, property):
+ '''determine the value of a property using the node, form or
+ filterspec
+ '''
propclass = self.properties[property]
if self.nodeid:
value = self.cl.get(self.nodeid, property, None)
- # TODO: remove this from the code ... it's only here for
- # handling schema changes, and they should be handled outside
- # of this code...
if isinstance(propclass, hyperdb.Multilink) and value is None:
- value = []
+ return []
+ return value
elif self.filterspec is not None:
if isinstance(propclass, hyperdb.Multilink):
- value = self.filterspec.get(property, [])
+ return self.filterspec.get(property, [])
else:
- value = self.filterspec.get(property, '')
+ return self.filterspec.get(property, '')
+ # TODO: pull the value from the form
+ if isinstance(propclass, hyperdb.Multilink):
+ return []
else:
- # TODO: pull the value from the form
- if isinstance(propclass, hyperdb.Multilink): value = []
- else: value = ''
+ return ''
+
+ def make_sort_function(self, classname):
+ '''Make a sort function for a given class
+ '''
+ linkcl = self.db.classes[classname]
+ if linkcl.getprops().has_key('order'):
+ sort_on = 'order'
+ else:
+ sort_on = linkcl.labelprop()
+ def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
+ return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
+ return sortfunc
+
+ def do_field(self, property, size=None, showid=0):
+ ''' display a property like the plain displayer, but in a text field
+ to be edited
+
+ Note: if you would prefer an option list style display for
+ link or multilink editing, use menu().
+ '''
+ if not self.nodeid and self.form is None and self.filterspec is None:
+ return _('[Field: not called from item]')
+
+ if size is None:
+ size = 30
+
+ propclass = self.properties[property]
+
+ # get the value
+ value = self.determine_value(property)
+
+ # now display
if (isinstance(propclass, hyperdb.String) or
isinstance(propclass, hyperdb.Date) or
isinstance(propclass, hyperdb.Interval)):
- size = size or 30
if value is None:
value = ''
else:
- value = cgi.escape(value)
+ value = cgi.escape(str(value))
value = '"'.join(value.split('"'))
s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
elif isinstance(propclass, hyperdb.Password):
- size = size or 30
s = '<input type="password" name="%s" size="%s">'%(property, size)
elif isinstance(propclass, hyperdb.Link):
+ sortfunc = self.make_sort_function(propclass.classname)
linkcl = self.db.classes[propclass.classname]
+ options = linkcl.list()
+ options.sort(sortfunc)
+ # TODO: make this a field display, not a menu one!
l = ['<select name="%s">'%property]
k = linkcl.labelprop()
- for optionid in linkcl.list():
+ if value is None:
+ s = 'selected '
+ else:
+ s = ''
+ l.append(_('<option %svalue="-1">- no selection -</option>')%s)
+ for optionid in options:
option = linkcl.get(optionid, k)
s = ''
if optionid == value:
lab = option
if size is not None and len(lab) > size:
lab = lab[:size-3] + '...'
+ lab = cgi.escape(lab)
l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
l.append('</select>')
s = '\n'.join(l)
elif isinstance(propclass, hyperdb.Multilink):
+ sortfunc = self.make_sort_function(propclass.classname)
linkcl = self.db.classes[propclass.classname]
- list = linkcl.list()
- height = height or min(len(list), 7)
+ if value:
+ value.sort(sortfunc)
+ # map the id to the label property
+ if not showid:
+ k = linkcl.labelprop()
+ value = [linkcl.get(v, k) for v in value]
+ value = cgi.escape(','.join(value))
+ s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
+ else:
+ s = _('Plain: bad propclass "%(propclass)s"')%locals()
+ return s
+
+ def do_multiline(self, property, rows=5, cols=40):
+ ''' display a string property in a multiline text edit field
+ '''
+ if not self.nodeid and self.form is None and self.filterspec is None:
+ return _('[Multiline: not called from item]')
+
+ propclass = self.properties[property]
+
+ # make sure this is a link property
+ if not isinstance(propclass, hyperdb.String):
+ return _('[Multiline: not a string]')
+
+ # get the value
+ value = self.determine_value(property)
+ if value is None:
+ value = ''
+
+ # display
+ return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
+ property, rows, cols, value)
+
+ def do_menu(self, property, size=None, height=None, showid=0):
+ ''' for a Link property, display a menu of the available choices
+ '''
+ if not self.nodeid and self.form is None and self.filterspec is None:
+ return _('[Field: not called from item]')
+
+ propclass = self.properties[property]
+
+ # make sure this is a link property
+ if not (isinstance(propclass, hyperdb.Link) or
+ isinstance(propclass, hyperdb.Multilink)):
+ return _('[Menu: not a link]')
+
+ # sort function
+ sortfunc = self.make_sort_function(propclass.classname)
+
+ # get the value
+ value = self.determine_value(property)
+
+ # display
+ if isinstance(propclass, hyperdb.Multilink):
+ linkcl = self.db.classes[propclass.classname]
+ options = linkcl.list()
+ options.sort(sortfunc)
+ height = height or min(len(options), 7)
l = ['<select multiple name="%s" size="%s">'%(property, height)]
k = linkcl.labelprop()
- for optionid in list:
+ for optionid in options:
option = linkcl.get(optionid, k)
s = ''
- if optionid in value:
+ if optionid in value or option in value:
s = 'selected '
if showid:
lab = '%s%s: %s'%(propclass.classname, optionid, option)
lab = option
if size is not None and len(lab) > size:
lab = lab[:size-3] + '...'
- l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
+ lab = cgi.escape(lab)
+ l.append('<option %svalue="%s">%s</option>'%(s, optionid,
+ lab))
l.append('</select>')
- s = '\n'.join(l)
- else:
- s = 'Plain: bad propclass "%s"'%propclass
- return s
-
-class Menu(Base):
- ''' for a Link property, display a menu of the available choices
- '''
- def __call__(self, property, size=None, height=None, showid=0):
- propclass = self.properties[property]
- if self.nodeid:
- value = self.cl.get(self.nodeid, property)
- else:
- # TODO: pull the value from the form
- if isinstance(propclass, hyperdb.Multilink): value = []
- else: value = None
+ return '\n'.join(l)
if isinstance(propclass, hyperdb.Link):
+ # force the value to be a single choice
+ if type(value) is types.ListType:
+ value = value[0]
linkcl = self.db.classes[propclass.classname]
l = ['<select name="%s">'%property]
k = linkcl.labelprop()
- for optionid in linkcl.list():
- option = linkcl.get(optionid, k)
- s = ''
- if optionid == value:
- s = 'selected '
- l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
- l.append('</select>')
- return '\n'.join(l)
- if isinstance(propclass, hyperdb.Multilink):
- linkcl = self.db.classes[propclass.classname]
- list = linkcl.list()
- height = height or min(len(list), 7)
- l = ['<select multiple name="%s" size="%s">'%(property, height)]
- k = linkcl.labelprop()
- for optionid in list:
+ s = ''
+ if value is None:
+ s = 'selected '
+ l.append(_('<option %svalue="-1">- no selection -</option>')%s)
+ options = linkcl.list()
+ options.sort(sortfunc)
+ for optionid in options:
option = linkcl.get(optionid, k)
s = ''
- if optionid in value:
+ if value in [optionid, option]:
s = 'selected '
if showid:
lab = '%s%s: %s'%(propclass.classname, optionid, option)
lab = option
if size is not None and len(lab) > size:
lab = lab[:size-3] + '...'
- l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
+ lab = cgi.escape(lab)
+ l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
l.append('</select>')
return '\n'.join(l)
- return '[Menu: not a link]'
+ return _('[Menu: not a link]')
-#XXX deviates from spec
-class Link(Base):
- ''' for a Link or Multilink property, display the names of the linked
- nodes, hyperlinked to the item views on those nodes
- for other properties, link to this node with the property as the text
- '''
- def __call__(self, property=None, **args):
+ #XXX deviates from spec
+ def do_link(self, property=None, is_download=0, showid=0):
+ '''For a Link or Multilink property, display the names of the linked
+ nodes, hyperlinked to the item views on those nodes.
+ For other properties, link to this node with the property as the
+ text.
+
+ If is_download is true, append the property value to the generated
+ URL so that the link may be used as a download link and the
+ downloaded file name is correct.
+ '''
if not self.nodeid and self.form is None:
- return '[Link: not called from item]'
+ return _('[Link: not called from item]')
+
+ # get the value
+ value = self.determine_value(property)
+ if not value:
+ return _('[no %(propname)s]')%{'propname':property.capitalize()}
+
propclass = self.properties[property]
- if self.nodeid:
- value = self.cl.get(self.nodeid, property)
- else:
- if isinstance(propclass, hyperdb.Multilink): value = []
- elif isinstance(propclass, hyperdb.Link): value = None
- else: value = ''
if isinstance(propclass, hyperdb.Link):
linkname = propclass.classname
- if value is None: return '[no %s]'%property.capitalize()
linkcl = self.db.classes[linkname]
k = linkcl.labelprop()
- linkvalue = linkcl.get(value, k)
- return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
+ linkvalue = cgi.escape(str(linkcl.get(value, k)))
+ if showid:
+ label = value
+ title = ' title="%s"'%linkvalue
+ # note ... this should be urllib.quote(linkcl.get(value, k))
+ else:
+ label = linkvalue
+ title = ''
+ if is_download:
+ return '<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
+ linkvalue, title, label)
+ else:
+ return '<a href="%s%s"%s>%s</a>'%(linkname, value, title, label)
if isinstance(propclass, hyperdb.Multilink):
linkname = propclass.classname
linkcl = self.db.classes[linkname]
k = linkcl.labelprop()
- if not value : return '[no %s]'%property.capitalize()
l = []
for value in value:
- linkvalue = linkcl.get(value, k)
- l.append('<a href="%s%s">%s</a>'%(linkname, value, linkvalue))
+ linkvalue = cgi.escape(str(linkcl.get(value, k)))
+ if showid:
+ label = value
+ title = ' title="%s"'%linkvalue
+ # note ... this should be urllib.quote(linkcl.get(value, k))
+ else:
+ label = linkvalue
+ title = ''
+ if is_download:
+ l.append('<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
+ linkvalue, title, label))
+ else:
+ l.append('<a href="%s%s"%s>%s</a>'%(linkname, value,
+ title, label))
return ', '.join(l)
- if isinstance(propclass, hyperdb.String):
- if value == '': value = '[no %s]'%property.capitalize()
- return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
+ if is_download:
+ return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
+ value, value)
+ else:
+ return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
-class Count(Base):
- ''' for a Multilink property, display a count of the number of links in
- the list
- '''
- def __call__(self, property, **args):
+ def do_count(self, property, **args):
+ ''' for a Multilink property, display a count of the number of links in
+ the list
+ '''
if not self.nodeid:
- return '[Count: not called from item]'
+ return _('[Count: not called from item]')
+
propclass = self.properties[property]
+ if not isinstance(propclass, hyperdb.Multilink):
+ return _('[Count: not a Multilink]')
+
+ # figure the length then...
value = self.cl.get(self.nodeid, property)
- if isinstance(propclass, hyperdb.Multilink):
- return str(len(value))
- return '[Count: not a Multilink]'
+ return str(len(value))
-# XXX pretty is definitely new ;)
-class Reldate(Base):
- ''' display a Date property in terms of an interval relative to the
- current date (e.g. "+ 3w", "- 2d").
+ # XXX pretty is definitely new ;)
+ def do_reldate(self, property, pretty=0):
+ ''' display a Date property in terms of an interval relative to the
+ current date (e.g. "+ 3w", "- 2d").
- with the 'pretty' flag, make it pretty
- '''
- def __call__(self, property, pretty=0):
+ with the 'pretty' flag, make it pretty
+ '''
if not self.nodeid and self.form is None:
- return '[Reldate: not called from item]'
+ return _('[Reldate: not called from item]')
+
propclass = self.properties[property]
- if isinstance(not propclass, hyperdb.Date):
- return '[Reldate: not a Date]'
+ if not isinstance(propclass, hyperdb.Date):
+ return _('[Reldate: not a Date]')
+
if self.nodeid:
value = self.cl.get(self.nodeid, property)
else:
- value = date.Date('.')
- interval = value - date.Date('.')
+ return ''
+ if not value:
+ return ''
+
+ # figure the interval
+ interval = date.Date('.') - value
if pretty:
if not self.nodeid:
- return 'now'
- pretty = interval.pretty()
- if pretty is None:
- pretty = value.pretty()
- return pretty
+ return _('now')
+ return interval.pretty()
return str(interval)
-class Download(Base):
- ''' show a Link("file") or Multilink("file") property using links that
- allow you to download files
- '''
- def __call__(self, property, **args):
+ def do_download(self, property, **args):
+ ''' show a Link("file") or Multilink("file") property using links that
+ allow you to download files
+ '''
if not self.nodeid:
- return '[Download: not called from item]'
- propclass = self.properties[property]
- value = self.cl.get(self.nodeid, property)
- if isinstance(propclass, hyperdb.Link):
- linkcl = self.db.classes[propclass.classname]
- linkvalue = linkcl.get(value, k)
- return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
- if isinstance(propclass, hyperdb.Multilink):
- linkcl = self.db.classes[propclass.classname]
- l = []
- for value in value:
- linkvalue = linkcl.get(value, k)
- l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
- return ', '.join(l)
- return '[Download: not a link]'
+ return _('[Download: not called from item]')
+ return self.do_link(property, is_download=1)
-class Checklist(Base):
- ''' for a Link or Multilink property, display checkboxes for the available
- choices to permit filtering
- '''
- def __call__(self, property, **args):
+ def do_checklist(self, property, **args):
+ ''' for a Link or Multilink property, display checkboxes for the
+ available choices to permit filtering
+ '''
propclass = self.properties[property]
if (not isinstance(propclass, hyperdb.Link) and not
isinstance(propclass, hyperdb.Multilink)):
- return '[Checklist: not a link]'
+ return _('[Checklist: not a link]')
# get our current checkbox state
if self.nodeid:
l = []
k = linkcl.labelprop()
for optionid in linkcl.list():
- option = linkcl.get(optionid, k)
+ option = cgi.escape(str(linkcl.get(optionid, k)))
if optionid in value or option in value:
checked = 'checked'
else:
checked = ''
l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
option, checked, property, option))
+
+ # for Links, allow the "unselected" option too
+ if isinstance(propclass, hyperdb.Link):
+ if value is None or '-1' in value:
+ checked = 'checked'
+ else:
+ checked = ''
+ l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
+ 'value="-1">')%(checked, property))
return '\n'.join(l)
-class Note(Base):
- ''' display a "note" field, which is a text area for entering a note to
- go along with a change.
- '''
- def __call__(self, rows=5, cols=80):
- # TODO: pull the value from the form
- return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
- cols)
-
-# XXX new function
-class List(Base):
- ''' list the items specified by property using the standard index for
- the class
- '''
- def __call__(self, property, reverse=0):
- propclass = self.properties[property]
- if isinstance(not propclass, hyperdb.Multilink):
- return '[List: not a Multilink]'
- fp = StringIO.StringIO()
- value = self.cl.get(self.nodeid, property)
+ def do_note(self, rows=5, cols=80):
+ ''' display a "note" field, which is a text area for entering a note to
+ go along with a change.
+ '''
+ # TODO: pull the value from the form
+ return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
+ '</textarea>'%(rows, cols)
+
+ # XXX new function
+ def do_list(self, property, reverse=0):
+ ''' list the items specified by property using the standard index for
+ the class
+ '''
+ propcl = self.properties[property]
+ if not isinstance(propcl, hyperdb.Multilink):
+ return _('[List: not a Multilink]')
+
+ value = self.determine_value(property)
+ if not value:
+ return ''
+
+ # sort, possibly revers and then re-stringify
+ value = map(int, value)
+ value.sort()
if reverse:
value.reverse()
- # TODO: really not happy with the way templates is passed on here
- index(fp, self.templates, self.db, propclass.classname, nodeids=value,
- show_display_form=0)
+ value = map(str, value)
+
+ # render the sub-index into a string
+ fp = StringIO.StringIO()
+ try:
+ write_save = self.client.write
+ self.client.write = fp.write
+ index = IndexTemplate(self.client, self.templates, propcl.classname)
+ index.render(nodeids=value, show_display_form=0)
+ finally:
+ self.client.write = write_save
+
return fp.getvalue()
-# XXX new function
-class History(Base):
- ''' list the history of the item
- '''
- def __call__(self, **args):
+ # XXX new function
+ def do_history(self, direction='descending'):
+ ''' list the history of the item
+
+ If "direction" is 'descending' then the most recent event will
+ be displayed first. If it is 'ascending' then the oldest event
+ will be displayed first.
+ '''
if self.nodeid is None:
- return "[History: node doesn't exist]"
+ return _("[History: node doesn't exist]")
l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
'<tr class="list-header">',
- '<td><span class="list-item"><strong>Date</strong></span></td>',
- '<td><span class="list-item"><strong>User</strong></span></td>',
- '<td><span class="list-item"><strong>Action</strong></span></td>',
- '<td><span class="list-item"><strong>Args</strong></span></td>']
-
- for id, date, user, action, args in self.cl.history(self.nodeid):
- l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
- date, user, action, args))
+ _('<th align=left><span class="list-item">Date</span></th>'),
+ _('<th align=left><span class="list-item">User</span></th>'),
+ _('<th align=left><span class="list-item">Action</span></th>'),
+ _('<th align=left><span class="list-item">Args</span></th>'),
+ '</tr>']
+
+ comments = {}
+ history = self.cl.history(self.nodeid)
+ history.sort()
+ if direction == 'descending':
+ history.reverse()
+ for id, evt_date, user, action, args in history:
+ date_s = str(evt_date).replace("."," ")
+ arg_s = ''
+ if action == 'link' and type(args) == type(()):
+ if len(args) == 3:
+ linkcl, linkid, key = args
+ arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+ linkcl, linkid, key)
+ else:
+ arg_s = str(args)
+
+ elif action == 'unlink' and type(args) == type(()):
+ if len(args) == 3:
+ linkcl, linkid, key = args
+ arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+ linkcl, linkid, key)
+ else:
+ arg_s = str(args)
+
+ elif type(args) == type({}):
+ cell = []
+ for k in args.keys():
+ # try to get the relevant property and treat it
+ # specially
+ try:
+ prop = self.properties[k]
+ except:
+ prop = None
+ if prop is not None:
+ if args[k] and (isinstance(prop, hyperdb.Multilink) or
+ isinstance(prop, hyperdb.Link)):
+ # figure what the link class is
+ classname = prop.classname
+ try:
+ linkcl = self.db.classes[classname]
+ except KeyError:
+ labelprop = None
+ comments[classname] = _('''The linked class
+ %(classname)s no longer exists''')%locals()
+ labelprop = linkcl.labelprop()
+
+ if isinstance(prop, hyperdb.Multilink) and \
+ len(args[k]) > 0:
+ ml = []
+ for linkid in args[k]:
+ label = classname + linkid
+ # if we have a label property, try to use it
+ # TODO: test for node existence even when
+ # there's no labelprop!
+ try:
+ if labelprop is not None:
+ label = linkcl.get(linkid, labelprop)
+ except IndexError:
+ comments['no_link'] = _('''<strike>The
+ linked node no longer
+ exists</strike>''')
+ ml.append('<strike>%s</strike>'%label)
+ else:
+ ml.append('<a href="%s%s">%s</a>'%(
+ classname, linkid, label))
+ cell.append('%s:\n %s'%(k, ',\n '.join(ml)))
+ elif isinstance(prop, hyperdb.Link) and args[k]:
+ label = classname + args[k]
+ # if we have a label property, try to use it
+ # TODO: test for node existence even when
+ # there's no labelprop!
+ if labelprop is not None:
+ try:
+ label = linkcl.get(args[k], labelprop)
+ except IndexError:
+ comments['no_link'] = _('''<strike>The
+ linked node no longer
+ exists</strike>''')
+ cell.append(' <strike>%s</strike>,\n'%label)
+ # "flag" this is done .... euwww
+ label = None
+ if label is not None:
+ cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
+ classname, args[k], label))
+
+ elif isinstance(prop, hyperdb.Date) and args[k]:
+ d = date.Date(args[k])
+ cell.append('%s: %s'%(k, str(d)))
+
+ elif isinstance(prop, hyperdb.Interval) and args[k]:
+ d = date.Interval(args[k])
+ cell.append('%s: %s'%(k, str(d)))
+
+ elif not args[k]:
+ cell.append('%s: (no value)\n'%k)
+
+ else:
+ cell.append('%s: %s\n'%(k, str(args[k])))
+ else:
+ # property no longer exists
+ comments['no_exist'] = _('''<em>The indicated property
+ no longer exists</em>''')
+ cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
+ arg_s = '<br />'.join(cell)
+ else:
+ # unkown event!!
+ comments['unknown'] = _('''<strong><em>This event is not
+ handled by the history display!</em></strong>''')
+ arg_s = '<strong><em>' + str(args) + '</em></strong>'
+ date_s = date_s.replace(' ', ' ')
+ l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
+ '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
+ user, action, arg_s))
+ if comments:
+ l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
+ for entry in comments.values():
+ l.append('<tr><td colspan=4>%s</td></tr>'%entry)
l.append('</table>')
return '\n'.join(l)
-# XXX new function
-class Submit(Base):
- ''' add a submit button for the item
- '''
- def __call__(self):
+ # XXX new function
+ def do_submit(self):
+ ''' add a submit button for the item
+ '''
if self.nodeid:
- return '<input type="submit" value="Submit Changes">'
+ return _('<input type="submit" name="submit" value="Submit Changes">')
elif self.form is not None:
- return '<input type="submit" value="Submit New Entry">'
+ return _('<input type="submit" name="submit" value="Submit New Entry">')
else:
- return '[Submit: not called from item]'
+ return _('[Submit: not called from item]')
+ def do_classhelp(self, classname, properties, label='?', width='400',
+ height='400'):
+ '''pop up a javascript window with class help
+ This generates a link to a popup window which displays the
+ properties indicated by "properties" of the class named by
+ "classname". The "properties" should be a comma-separated list
+ (eg. 'id,name,description').
+
+ You may optionally override the label displayed, the width and
+ height. The popup window will be resizable and scrollable.
+ '''
+ return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
+ 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
+ properties, width, height, label)
#
# INDEX TEMPLATES
#
class IndexTemplateReplace:
+ '''Regular-expression based parser that turns the template into HTML.
+ '''
def __init__(self, globals, locals, props):
self.globals = globals
self.locals = locals
self.props = props
- def go(self, text, replace=re.compile(
- r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
- r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
- return replace.sub(self, text)
+ replace=re.compile(
+ r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
+ r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
+ def go(self, text):
+ return self.replace.sub(self, text)
def __call__(self, m, filter=None, columns=None, sort=None, group=None):
if m.group('name'):
if m.group('name') in self.props:
text = m.group('text')
replace = IndexTemplateReplace(self.globals, {}, self.props)
- return replace.go(m.group('text'))
+ return replace.go(text)
else:
return ''
if m.group('display'):
command = m.group('command')
return eval(command, self.globals, self.locals)
- print '*** unhandled match', m.groupdict()
-
-def sortby(sort_name, columns, filter, sort, group, filterspec):
- l = []
- w = l.append
- for k, v in filterspec.items():
- k = urllib.quote(k)
- if type(v) == type([]):
- w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
- else:
- w('%s=%s'%(k, urllib.quote(v)))
- if columns:
- w(':columns=%s'%','.join(map(urllib.quote, columns)))
- if filter:
- w(':filter=%s'%','.join(map(urllib.quote, filter)))
- if group:
- w(':group=%s'%','.join(map(urllib.quote, group)))
- m = []
- s_dir = ''
- for name in sort:
- dir = name[0]
- if dir == '-':
- name = name[1:]
- else:
- dir = ''
- if sort_name == name:
- if dir == '-':
- s_dir = ''
- else:
- s_dir = '-'
+ return '*** unhandled match: %s'%str(m.groupdict())
+
+class IndexTemplate(TemplateFunctions):
+ '''Templating functionality specifically for index pages
+ '''
+ def __init__(self, client, templates, classname):
+ TemplateFunctions.__init__(self)
+ self.client = client
+ self.instance = client.instance
+ self.templates = templates
+ self.classname = classname
+
+ # derived
+ self.db = self.client.db
+ self.cl = self.db.classes[self.classname]
+ self.properties = self.cl.getprops()
+
+ col_re=re.compile(r'<property\s+name="([^>]+)">')
+ def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
+ show_display_form=1, nodeids=None, show_customization=1):
+ self.filterspec = filterspec
+
+ w = self.client.write
+
+ # get the filter template
+ try:
+ filter_template = open(os.path.join(self.templates,
+ self.classname+'.filter')).read()
+ all_filters = self.col_re.findall(filter_template)
+ except IOError, error:
+ if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+ filter_template = None
+ all_filters = []
+
+ # XXX deviate from spec here ...
+ # load the index section template and figure the default columns from it
+ try:
+ template = open(os.path.join(self.templates,
+ self.classname+'.index')).read()
+ except IOError, error:
+ if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+ raise MissingTemplateError, self.classname+'.index'
+ all_columns = self.col_re.findall(template)
+ if not columns:
+ columns = []
+ for name in all_columns:
+ columns.append(name)
else:
- m.append(dir+urllib.quote(name))
- m.insert(0, s_dir+urllib.quote(sort_name))
- # so things don't get completely out of hand, limit the sort to two columns
- w(':sort=%s'%','.join(m[:2]))
- return '&'.join(l)
-
-def index(client, templates, db, classname, filterspec={}, filter=[],
- columns=[], sort=[], group=[], show_display_form=1, nodeids=None,
- show_customization=1,
- col_re=re.compile(r'<property\s+name="([^>]+)">')):
- globals = {
- 'plain': Plain(db, templates, classname, filterspec=filterspec),
- 'field': Field(db, templates, classname, filterspec=filterspec),
- 'menu': Menu(db, templates, classname, filterspec=filterspec),
- 'link': Link(db, templates, classname, filterspec=filterspec),
- 'count': Count(db, templates, classname, filterspec=filterspec),
- 'reldate': Reldate(db, templates, classname, filterspec=filterspec),
- 'download': Download(db, templates, classname, filterspec=filterspec),
- 'checklist': Checklist(db, templates, classname, filterspec=filterspec),
- 'list': List(db, templates, classname, filterspec=filterspec),
- 'history': History(db, templates, classname, filterspec=filterspec),
- 'submit': Submit(db, templates, classname, filterspec=filterspec),
- 'note': Note(db, templates, classname, filterspec=filterspec)
- }
- cl = db.classes[classname]
- properties = cl.getprops()
- w = client.write
- w('<form>')
-
- try:
- template = open(os.path.join(templates, classname+'.filter')).read()
- all_filters = col_re.findall(template)
- except IOError, error:
- if error.errno != errno.ENOENT: raise
- template = None
- all_filters = []
- if template and filter:
+ # re-sort columns to be the same order as all_columns
+ l = []
+ for name in all_columns:
+ if name in columns:
+ l.append(name)
+ columns = l
+
# display the filter section
- w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
- w('<tr class="location-bar">')
- w(' <th align="left" colspan="2">Filter specification...</th>')
- w('</tr>')
- replace = IndexTemplateReplace(globals, locals(), filter)
- w(replace.go(template))
- w('<tr class="location-bar"><td width="1%%"> </td>')
- w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
+ if (show_display_form and
+ self.instance.FILTER_POSITION in ('top and bottom', 'top')):
+ w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
+ self.filter_section(filter_template, filter, columns, group,
+ all_filters, all_columns, show_customization)
+ # make sure that the sorting doesn't get lost either
+ if sort:
+ w('<input type="hidden" name=":sort" value="%s">'%
+ ','.join(sort))
+ w('</form>\n')
+
+
+ # now display the index section
+ w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
+ w('<tr class="list-header">\n')
+ for name in columns:
+ cname = name.capitalize()
+ if show_display_form:
+ sb = self.sortby(name, filterspec, columns, filter, group, sort)
+ anchor = "%s?%s"%(self.classname, sb)
+ w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
+ anchor, cname))
+ else:
+ w('<td><span class="list-header">%s</span></td>\n'%cname)
+ w('</tr>\n')
+
+ # this stuff is used for group headings - optimise the group names
+ old_group = None
+ group_names = []
+ if group:
+ for name in group:
+ if name[0] == '-': group_names.append(name[1:])
+ else: group_names.append(name)
+
+ # now actually loop through all the nodes we get from the filter and
+ # apply the template
+ if nodeids is None:
+ nodeids = self.cl.filter(filterspec, sort, group)
+ for nodeid in nodeids:
+ # check for a group heading
+ if group_names:
+ this_group = [self.cl.get(nodeid, name, _('[no value]'))
+ for name in group_names]
+ if this_group != old_group:
+ l = []
+ for name in group_names:
+ prop = self.properties[name]
+ if isinstance(prop, hyperdb.Link):
+ group_cl = self.db.classes[prop.classname]
+ key = group_cl.getkey()
+ value = self.cl.get(nodeid, name)
+ if value is None:
+ l.append(_('[unselected %(classname)s]')%{
+ 'classname': prop.classname})
+ else:
+ l.append(group_cl.get(self.cl.get(nodeid,
+ name), key))
+ elif isinstance(prop, hyperdb.Multilink):
+ group_cl = self.db.classes[prop.classname]
+ key = group_cl.getkey()
+ for value in self.cl.get(nodeid, name):
+ l.append(group_cl.get(value, key))
+ else:
+ value = self.cl.get(nodeid, name, _('[no value]'))
+ if value is None:
+ value = _('[empty %(name)s]')%locals()
+ else:
+ value = str(value)
+ l.append(value)
+ w('<tr class="section-bar">'
+ '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
+ len(columns), ', '.join(l)))
+ old_group = this_group
+
+ # display this node's row
+ replace = IndexTemplateReplace(self.globals, locals(), columns)
+ self.nodeid = nodeid
+ w(replace.go(template))
+ self.nodeid = None
+
w('</table>')
- # If the filters aren't being displayed, then hide their current
- # value in the form
- if not filter:
- for k, v in filterspec.items():
- if type(v) == type([]): v = ','.join(v)
- w('<input type="hidden" name="%s" value="%s">'%(k, v))
-
- # make sure that the sorting doesn't get lost either
- if sort:
- w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
-
- # XXX deviate from spec here ...
- # load the index section template and figure the default columns from it
- template = open(os.path.join(templates, classname+'.index')).read()
- all_columns = col_re.findall(template)
- if not columns:
- columns = []
- for name in all_columns:
- columns.append(name)
- else:
- # re-sort columns to be the same order as all_columns
- l = []
- for name in all_columns:
- if name in columns:
- l.append(name)
- columns = l
-
- # now add in the filter/columns/group/etc config table form
- w('<input type="hidden" name="show_customization" value="%s">' %
- show_customization )
- w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
- names = []
- for name in cl.getprops().keys():
- if name in all_filters or name in all_columns:
- names.append(name)
- w('<tr class="location-bar">')
- if show_customization:
- action = '-'
- else:
- action = '+'
- # hide the values for filters, columns and grouping in the form
- # if the customization widget is not visible
- for name in names:
- if all_filters and name in filter:
- w('<input type="hidden" name=":filter" value="%s">' % name)
- if all_columns and name in columns:
- w('<input type="hidden" name=":columns" value="%s">' % name)
- if all_columns and name in group:
- w('<input type="hidden" name=":group" value="%s">' % name)
-
- if show_display_form:
+ # display the filter section
+ if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
+ self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
+ w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
+ self.filter_section(filter_template, filter, columns, group,
+ all_filters, all_columns, show_customization)
+ # make sure that the sorting doesn't get lost either
+ if sort:
+ w('<input type="hidden" name=":sort" value="%s">'%
+ ','.join(sort))
+ w('</form>\n')
+
+
+ def filter_section(self, template, filter, columns, group, all_filters,
+ all_columns, show_customization):
+
+ w = self.client.write
+
+ # wrap the template in a single table to ensure the whole widget
+ # is displayed at once
+ w('<table><tr><td>')
+
+ if template and filter:
+ # display the filter section
+ w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
+ w('<tr class="location-bar">')
+ w(_(' <th align="left" colspan="2">Filter specification...</th>'))
+ w('</tr>')
+ replace = IndexTemplateReplace(self.globals, locals(), filter)
+ w(replace.go(template))
+ w('<tr class="location-bar"><td width="1%%"> </td>')
+ w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
+ w('</table>')
+
+ # now add in the filter/columns/group/etc config table form
+ w('<input type="hidden" name="show_customization" value="%s">' %
+ show_customization )
+ w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
+ names = []
+ seen = {}
+ for name in all_filters + all_columns:
+ if self.properties.has_key(name) and not seen.has_key(name):
+ names.append(name)
+ seen[name] = 1
+ if show_customization:
+ action = '-'
+ else:
+ action = '+'
+ # hide the values for filters, columns and grouping in the form
+ # if the customization widget is not visible
+ for name in names:
+ if all_filters and name in filter:
+ w('<input type="hidden" name=":filter" value="%s">' % name)
+ if all_columns and name in columns:
+ w('<input type="hidden" name=":columns" value="%s">' % name)
+ if all_columns and name in group:
+ w('<input type="hidden" name=":group" value="%s">' % name)
+
# TODO: The widget style can go into the stylesheet
- w('<th align="left" colspan=%s>'
+ w(_('<th align="left" colspan=%s>'
'<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
- 'customisation...</th></tr>\n'%(len(names)+1, action))
- if show_customization:
- w('<tr class="location-bar"><th> </th>')
+ 'customisation...</th></tr>\n')%(len(names)+1, action))
+
+ if not show_customization:
+ w('</table>\n')
+ return
+
+ w('<tr class="location-bar"><th> </th>')
+ for name in names:
+ w('<th>%s</th>'%name.capitalize())
+ w('</tr>\n')
+
+ # Filter
+ if all_filters:
+ w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
+ for name in names:
+ if name not in all_filters:
+ w('<td> </td>')
+ continue
+ if name in filter: checked=' checked'
+ else: checked=''
+ w('<td align=middle>\n')
+ w(' <input type="checkbox" name=":filter" value="%s" '
+ '%s></td>\n'%(name, checked))
+ w('</tr>\n')
+
+ # Columns
+ if all_columns:
+ w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
for name in names:
- w('<th>%s</th>'%name.capitalize())
+ if name not in all_columns:
+ w('<td> </td>')
+ continue
+ if name in columns: checked=' checked'
+ else: checked=''
+ w('<td align=middle>\n')
+ w(' <input type="checkbox" name=":columns" value="%s"'
+ '%s></td>\n'%(name, checked))
w('</tr>\n')
- # Filter
- if all_filters:
- w('<tr><th width="1%" align=right class="location-bar">'
- 'Filters</th>\n')
- for name in names:
- if name not in all_filters:
- w('<td> </td>')
- continue
- if name in filter: checked=' checked'
- else: checked=''
- w('<td align=middle>\n')
- w(' <input type="checkbox" name=":filter" value="%s" '
- '%s></td>\n'%(name, checked))
- w('</tr>\n')
-
- # Columns
- if all_columns:
- w('<tr><th width="1%" align=right class="location-bar">'
- 'Columns</th>\n')
- for name in names:
- if name not in all_columns:
- w('<td> </td>')
- continue
- if name in columns: checked=' checked'
- else: checked=''
- w('<td align=middle>\n')
- w(' <input type="checkbox" name=":columns" value="%s"'
- '%s></td>\n'%(name, checked))
- w('</tr>\n')
-
- # Grouping
- w('<tr><th width="1%" align=right class="location-bar">'
- 'Grouping</th>\n')
- for name in names:
- prop = properties[name]
- if name not in all_columns:
- w('<td> </td>')
- continue
- if name in group: checked=' checked'
- else: checked=''
- w('<td align=middle>\n')
- w(' <input type="checkbox" name=":group" value="%s"'
- '%s></td>\n'%(name, checked))
- w('</tr>\n')
-
- w('<tr class="location-bar"><td width="1%"> </td>')
- w('<td colspan="%s">'%len(names))
- w('<input type="submit" name="action" value="Redisplay"></td>')
+ # Grouping
+ w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
+ for name in names:
+ if name not in all_columns:
+ w('<td> </td>')
+ continue
+ if name in group: checked=' checked'
+ else: checked=''
+ w('<td align=middle>\n')
+ w(' <input type="checkbox" name=":group" value="%s"'
+ '%s></td>\n'%(name, checked))
w('</tr>\n')
+ w('<tr class="location-bar"><td width="1%"> </td>')
+ w('<td colspan="%s">'%len(names))
+ w(_('<input type="submit" name="action" value="Redisplay"></td>'))
+ w('</tr>\n')
w('</table>\n')
- w('</form>\n')
-
- # now display the index section
- w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
- w('<tr class="list-header">\n')
- for name in columns:
- cname = name.capitalize()
- if show_display_form:
- anchor = "%s?%s"%(classname, sortby(name, columns, filter,
- sort, group, filterspec))
- w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
- anchor, cname))
- else:
- w('<td><span class="list-header">%s</span></td>\n'%cname)
- w('</tr>\n')
-
- # this stuff is used for group headings - optimise the group names
- old_group = None
- group_names = []
- if group:
- for name in group:
- if name[0] == '-': group_names.append(name[1:])
- else: group_names.append(name)
-
- # now actually loop through all the nodes we get from the filter and
- # apply the template
- if nodeids is None:
- nodeids = cl.filter(filterspec, sort, group)
- for nodeid in nodeids:
- # check for a group heading
- if group_names:
- this_group = [cl.get(nodeid, name) for name in group_names]
- if this_group != old_group:
- l = []
- for name in group_names:
- prop = properties[name]
- if isinstance(prop, hyperdb.Link):
- group_cl = db.classes[prop.classname]
- key = group_cl.getkey()
- value = cl.get(nodeid, name)
- if value is None:
- l.append('[unselected %s]'%prop.classname)
- else:
- l.append(group_cl.get(cl.get(nodeid, name), key))
- elif isinstance(prop, hyperdb.Multilink):
- group_cl = db.classes[prop.classname]
- key = group_cl.getkey()
- for value in cl.get(nodeid, name):
- l.append(group_cl.get(value, key))
- else:
- value = cl.get(nodeid, name)
- if value is None:
- value = '[empty %s]'%name
- else:
- value = str(value)
- l.append(value)
- w('<tr class="section-bar">'
- '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
- len(columns), ', '.join(l)))
- old_group = this_group
- # display this node's row
- for value in globals.values():
- if hasattr(value, 'nodeid'):
- value.nodeid = nodeid
- replace = IndexTemplateReplace(globals, locals(), columns)
- w(replace.go(template))
+ # and the outer table
+ w('</td></tr></table>')
- w('</table>')
+ def sortby(self, sort_name, filterspec, columns, filter, group, sort):
+ l = []
+ w = l.append
+ for k, v in filterspec.items():
+ k = urllib.quote(k)
+ if type(v) == type([]):
+ w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
+ else:
+ w('%s=%s'%(k, urllib.quote(v)))
+ if columns:
+ w(':columns=%s'%','.join(map(urllib.quote, columns)))
+ if filter:
+ w(':filter=%s'%','.join(map(urllib.quote, filter)))
+ if group:
+ w(':group=%s'%','.join(map(urllib.quote, group)))
+ m = []
+ s_dir = ''
+ for name in sort:
+ dir = name[0]
+ if dir == '-':
+ name = name[1:]
+ else:
+ dir = ''
+ if sort_name == name:
+ if dir == '-':
+ s_dir = ''
+ else:
+ s_dir = '-'
+ else:
+ m.append(dir+urllib.quote(name))
+ m.insert(0, s_dir+urllib.quote(sort_name))
+ # so things don't get completely out of hand, limit the sort to
+ # two columns
+ w(':sort=%s'%','.join(m[:2]))
+ return '&'.join(l)
#
# ITEM TEMPLATES
#
class ItemTemplateReplace:
+ '''Regular-expression based parser that turns the template into HTML.
+ '''
def __init__(self, globals, locals, cl, nodeid):
self.globals = globals
self.locals = locals
self.cl = cl
self.nodeid = nodeid
- def go(self, text, replace=re.compile(
- r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
- r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
- return replace.sub(self, text)
+ replace=re.compile(
+ r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
+ r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
+ def go(self, text):
+ return self.replace.sub(self, text)
def __call__(self, m, filter=None, columns=None, sort=None, group=None):
if m.group('name'):
if m.group('display'):
command = m.group('command')
return eval(command, self.globals, self.locals)
- print '*** unhandled match', m.groupdict()
-
-def item(client, templates, db, classname, nodeid, replace=re.compile(
- r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
- r'(?P<endprop></property>)|'
- r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
-
- globals = {
- 'plain': Plain(db, templates, classname, nodeid),
- 'field': Field(db, templates, classname, nodeid),
- 'menu': Menu(db, templates, classname, nodeid),
- 'link': Link(db, templates, classname, nodeid),
- 'count': Count(db, templates, classname, nodeid),
- 'reldate': Reldate(db, templates, classname, nodeid),
- 'download': Download(db, templates, classname, nodeid),
- 'checklist': Checklist(db, templates, classname, nodeid),
- 'list': List(db, templates, classname, nodeid),
- 'history': History(db, templates, classname, nodeid),
- 'submit': Submit(db, templates, classname, nodeid),
- 'note': Note(db, templates, classname, nodeid)
- }
-
- cl = db.classes[classname]
- properties = cl.getprops()
-
- if properties.has_key('type') and properties.has_key('content'):
- pass
- # XXX we really want to return this as a downloadable...
- # currently I handle this at a higher level by detecting 'file'
- # designators...
-
- w = client.write
- w('<form action="%s%s">'%(classname, nodeid))
- s = open(os.path.join(templates, classname+'.item')).read()
- replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
- w(replace.go(s))
- w('</form>')
-
-
-def newitem(client, templates, db, classname, form, replace=re.compile(
- r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
- r'(?P<endprop></property>)|'
- r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
- globals = {
- 'plain': Plain(db, templates, classname, form=form),
- 'field': Field(db, templates, classname, form=form),
- 'menu': Menu(db, templates, classname, form=form),
- 'link': Link(db, templates, classname, form=form),
- 'count': Count(db, templates, classname, form=form),
- 'reldate': Reldate(db, templates, classname, form=form),
- 'download': Download(db, templates, classname, form=form),
- 'checklist': Checklist(db, templates, classname, form=form),
- 'list': List(db, templates, classname, form=form),
- 'history': History(db, templates, classname, form=form),
- 'submit': Submit(db, templates, classname, form=form),
- 'note': Note(db, templates, classname, form=form)
- }
-
- cl = db.classes[classname]
- properties = cl.getprops()
-
- w = client.write
- try:
- s = open(os.path.join(templates, classname+'.newitem')).read()
- except:
- s = open(os.path.join(templates, classname+'.item')).read()
- w('<form action="new%s" method="POST" enctype="multipart/form-data">'%classname)
- for key in form.keys():
- if key[0] == ':':
- value = form[key].value
- if type(value) != type([]): value = [value]
- for value in value:
- w('<input type="hidden" name="%s" value="%s">'%(key, value))
- replace = ItemTemplateReplace(globals, locals(), None, None)
- w(replace.go(s))
- w('</form>')
+ return '*** unhandled match: %s'%str(m.groupdict())
+
+
+class ItemTemplate(TemplateFunctions):
+ '''Templating functionality specifically for item (node) display
+ '''
+ def __init__(self, client, templates, classname):
+ TemplateFunctions.__init__(self)
+ self.client = client
+ self.instance = client.instance
+ self.templates = templates
+ self.classname = classname
+
+ # derived
+ self.db = self.client.db
+ self.cl = self.db.classes[self.classname]
+ self.properties = self.cl.getprops()
+
+ def render(self, nodeid):
+ self.nodeid = nodeid
+
+ if (self.properties.has_key('type') and
+ self.properties.has_key('content')):
+ pass
+ # XXX we really want to return this as a downloadable...
+ # currently I handle this at a higher level by detecting 'file'
+ # designators...
+
+ w = self.client.write
+ w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
+ self.classname, nodeid))
+ s = open(os.path.join(self.templates, self.classname+'.item')).read()
+ replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
+ w(replace.go(s))
+ w('</form>')
+
+
+class NewItemTemplate(TemplateFunctions):
+ '''Templating functionality specifically for NEW item (node) display
+ '''
+ def __init__(self, client, templates, classname):
+ TemplateFunctions.__init__(self)
+ self.client = client
+ self.instance = client.instance
+ self.templates = templates
+ self.classname = classname
+
+ # derived
+ self.db = self.client.db
+ self.cl = self.db.classes[self.classname]
+ self.properties = self.cl.getprops()
+
+ def render(self, form):
+ self.form = form
+ w = self.client.write
+ c = self.classname
+ try:
+ s = open(os.path.join(self.templates, c+'.newitem')).read()
+ except IOError:
+ s = open(os.path.join(self.templates, c+'.item')).read()
+ w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
+ for key in form.keys():
+ if key[0] == ':':
+ value = form[key].value
+ if type(value) != type([]): value = [value]
+ for value in value:
+ w('<input type="hidden" name="%s" value="%s">'%(key, value))
+ replace = ItemTemplateReplace(self.globals, locals(), None, None)
+ w(replace.go(s))
+ w('</form>')
#
# $Log: not supported by cvs2svn $
+# Revision 1.88 2002/04/24 08:34:35 rochecompaan
+# Sorting was applied to all nodes of the MultiLink class instead of
+# the nodes that are actually linked to in the "field" template
+# function. This adds about 20+ seconds in the display of an issue if
+# your database has a 1000 or more issue in it.
+#
+# Revision 1.87 2002/04/03 06:12:46 richard
+# Fix for date properties as labels.
+#
+# Revision 1.86 2002/04/03 05:54:31 richard
+# Fixed serialisation problem by moving the serialisation step out of the
+# hyperdb.Class (get, set) into the hyperdb.Database.
+#
+# Also fixed htmltemplate after the showid changes I made yesterday.
+#
+# Unit tests for all of the above written.
+#
+# Revision 1.85 2002/04/02 01:40:58 richard
+# . link() htmltemplate function now has a "showid" option for links and
+# multilinks. When true, it only displays the linked node id as the anchor
+# text. The link value is displayed as a tooltip using the title anchor
+# attribute.
+#
+# Revision 1.84 2002/03/29 19:41:48 rochecompaan
+# . Fixed display of mutlilink properties when using the template
+# functions, menu and plain.
+#
+# Revision 1.83 2002/02/27 04:14:31 richard
+# Ran it through pychecker, made fixes
+#
+# Revision 1.82 2002/02/21 23:11:45 richard
+# . fixed some problems in date calculations (calendar.py doesn't handle over-
+# and under-flow). Also, hour/minute/second intervals may now be more than
+# 99 each.
+#
+# Revision 1.81 2002/02/21 07:21:38 richard
+# docco
+#
+# Revision 1.80 2002/02/21 07:19:08 richard
+# ... and label, width and height control for extra flavour!
+#
+# Revision 1.79 2002/02/21 06:57:38 richard
+# . Added popup help for classes using the classhelp html template function.
+# - add <display call="classhelp('priority', 'id,name,description')">
+# to an item page, and it generates a link to a popup window which displays
+# the id, name and description for the priority class. The description
+# field won't exist in most installations, but it will be added to the
+# default templates.
+#
+# Revision 1.78 2002/02/21 06:23:00 richard
+# *** empty log message ***
+#
+# Revision 1.77 2002/02/20 05:05:29 richard
+# . Added simple editing for classes that don't define a templated interface.
+# - access using the admin "class list" interface
+# - limited to admin-only
+# - requires the csv module from object-craft (url given if it's missing)
+#
+# Revision 1.76 2002/02/16 09:10:52 richard
+# oops
+#
+# Revision 1.75 2002/02/16 08:43:23 richard
+# . #517906 ] Attribute order in "View customisation"
+#
+# Revision 1.74 2002/02/16 08:39:42 richard
+# . #516854 ] "My Issues" and redisplay
+#
+# Revision 1.73 2002/02/15 07:08:44 richard
+# . Alternate email addresses are now available for users. See the MIGRATION
+# file for info on how to activate the feature.
+#
+# Revision 1.72 2002/02/14 23:39:18 richard
+# . All forms now have "double-submit" protection when Javascript is enabled
+# on the client-side.
+#
+# Revision 1.71 2002/01/23 06:15:24 richard
+# real (non-string, duh) sorting of lists by node id
+#
+# Revision 1.70 2002/01/23 05:47:57 richard
+# more HTML template cleanup and unit tests
+#
+# Revision 1.69 2002/01/23 05:10:27 richard
+# More HTML template cleanup and unit tests.
+# - download() now implemented correctly, replacing link(is_download=1) [fixed in the
+# templates, but link(is_download=1) will still work for existing templates]
+#
+# Revision 1.68 2002/01/22 22:55:28 richard
+# . htmltemplate list() wasn't sorting...
+#
+# Revision 1.67 2002/01/22 22:46:22 richard
+# more htmltemplate cleanups and unit tests
+#
+# Revision 1.66 2002/01/22 06:35:40 richard
+# more htmltemplate tests and cleanup
+#
+# Revision 1.65 2002/01/22 00:12:06 richard
+# Wrote more unit tests for htmltemplate, and while I was at it, I polished
+# off the implementation of some of the functions so they behave sanely.
+#
+# Revision 1.64 2002/01/21 03:25:59 richard
+# oops
+#
+# Revision 1.63 2002/01/21 02:59:10 richard
+# Fixed up the HTML display of history so valid links are actually displayed.
+# Oh for some unit tests! :(
+#
+# Revision 1.62 2002/01/18 08:36:12 grubert
+# . add nowrap to history table date cell i.e. <td nowrap ...
+#
+# Revision 1.61 2002/01/17 23:04:53 richard
+# . much nicer history display (actualy real handling of property types etc)
+#
+# Revision 1.60 2002/01/17 08:48:19 grubert
+# . display superseder as html link in history.
+#
+# Revision 1.59 2002/01/17 07:58:24 grubert
+# . display links a html link in history.
+#
+# Revision 1.58 2002/01/15 00:50:03 richard
+# #502949 ] index view for non-issues and redisplay
+#
+# Revision 1.57 2002/01/14 23:31:21 richard
+# reverted the change that had plain() hyperlinking the link displays -
+# that's what link() is for!
+#
+# Revision 1.56 2002/01/14 07:04:36 richard
+# . plain rendering of links in the htmltemplate now generate a hyperlink to
+# the linked node's page.
+# ... this allows a display very similar to bugzilla's where you can actually
+# find out information about the linked node.
+#
+# Revision 1.55 2002/01/14 06:45:03 richard
+# . #502953 ] nosy-like treatment of other multilinks
+# ... had to revert most of the previous change to the multilink field
+# display... not good.
+#
+# Revision 1.54 2002/01/14 05:16:51 richard
+# The submit buttons need a name attribute or mozilla won't submit without a
+# file upload. Yeah, that's bloody obscure. Grr.
+#
+# Revision 1.53 2002/01/14 04:03:32 richard
+# How about that ... date fields have never worked ...
+#
+# Revision 1.52 2002/01/14 02:20:14 richard
+# . changed all config accesses so they access either the instance or the
+# config attriubute on the db. This means that all config is obtained from
+# instance_config instead of the mish-mash of classes. This will make
+# switching to a ConfigParser setup easier too, I hope.
+#
+# At a minimum, this makes migration a _little_ easier (a lot easier in the
+# 0.5.0 switch, I hope!)
+#
+# Revision 1.51 2002/01/10 10:02:15 grubert
+# In do_history: replace "." in date by " " so html wraps more sensible.
+# Should this be done in date's string converter ?
+#
+# Revision 1.50 2002/01/05 02:35:10 richard
+# I18N'ification
+#
+# Revision 1.49 2001/12/20 15:43:01 rochecompaan
+# Features added:
+# . Multilink properties are now displayed as comma separated values in
+# a textbox
+# . The add user link is now only visible to the admin user
+# . Modified the mail gateway to reject submissions from unknown
+# addresses if ANONYMOUS_ACCESS is denied
+#
+# Revision 1.48 2001/12/20 06:13:24 rochecompaan
+# Bugs fixed:
+# . Exception handling in hyperdb for strings-that-look-like numbers got
+# lost somewhere
+# . Internet Explorer submits full path for filename - we now strip away
+# the path
+# Features added:
+# . Link and multilink properties are now displayed sorted in the cgi
+# interface
+#
+# Revision 1.47 2001/11/26 22:55:56 richard
+# Feature:
+# . Added INSTANCE_NAME to configuration - used in web and email to identify
+# the instance.
+# . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
+# signature info in e-mails.
+# . Some more flexibility in the mail gateway and more error handling.
+# . Login now takes you to the page you back to the were denied access to.
+#
+# Fixed:
+# . Lots of bugs, thanks Roché and others on the devel mailing list!
+#
+# Revision 1.46 2001/11/24 00:53:12 jhermann
+# "except:" is bad, bad , bad!
+#
+# Revision 1.45 2001/11/22 15:46:42 jhermann
+# Added module docstrings to all modules.
+#
+# Revision 1.44 2001/11/21 23:35:45 jhermann
+# Added globbing for win32, and sample marking in a 2nd file to test it
+#
+# Revision 1.43 2001/11/21 04:04:43 richard
+# *sigh* more missing value handling
+#
+# Revision 1.42 2001/11/21 03:40:54 richard
+# more new property handling
+#
+# Revision 1.41 2001/11/15 10:26:01 richard
+# . missing "return" in filter_section (thanks Roch'e Compaan)
+#
+# Revision 1.40 2001/11/03 01:56:51 richard
+# More HTML compliance fixes. This will probably fix the Netscape problem
+# too.
+#
+# Revision 1.39 2001/11/03 01:43:47 richard
+# Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
+#
+# Revision 1.38 2001/10/31 06:58:51 richard
+# Added the wrap="hard" attribute to the textarea of the note field so the
+# messages wrap sanely.
+#
+# Revision 1.37 2001/10/31 06:24:35 richard
+# Added do_stext to htmltemplate, thanks Brad Clements.
+#
+# Revision 1.36 2001/10/28 22:51:38 richard
+# Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
+#
+# Revision 1.35 2001/10/24 00:04:41 richard
+# Removed the "infinite authentication loop", thanks Roch'e
+#
+# Revision 1.34 2001/10/23 22:56:36 richard
+# Bugfix in filter "widget" placement, thanks Roch'e
+#
+# Revision 1.33 2001/10/23 01:00:18 richard
+# Re-enabled login and registration access after lopping them off via
+# disabling access for anonymous users.
+# Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
+# a couple of bugs while I was there. Probably introduced a couple, but
+# things seem to work OK at the moment.
+#
+# Revision 1.32 2001/10/22 03:25:01 richard
+# Added configuration for:
+# . anonymous user access and registration (deny/allow)
+# . filter "widget" location on index page (top, bottom, both)
+# Updated some documentation.
+#
+# Revision 1.31 2001/10/21 07:26:35 richard
+# feature #473127: Filenames. I modified the file.index and htmltemplate
+# source so that the filename is used in the link and the creation
+# information is displayed.
+#
+# Revision 1.30 2001/10/21 04:44:50 richard
+# bug #473124: UI inconsistency with Link fields.
+# This also prompted me to fix a fairly long-standing usability issue -
+# that of being able to turn off certain filters.
+#
+# Revision 1.29 2001/10/21 00:17:56 richard
+# CGI interface view customisation section may now be hidden (patch from
+# Roch'e Compaan.)
+#
# Revision 1.28 2001/10/21 00:00:16 richard
# Fixed Checklist function - wasn't always working on a list.
#