index d68381184f23eac260608d270559d6d389e00ca6..a3578e6805c46fe6aa6479ed967b47b7202435e3 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.52 2002-01-14 02:20:14 richard Exp $
+# $Id: htmltemplate.py,v 1.112 2002-08-19 00:21:37 richard Exp $
__doc__ = """
Template engine.
-"""
-
-import os, re, StringIO, urllib, cgi, errno
-import hyperdb, date, password
-from i18n import _
+Three types of template files exist:
+ .index used by IndexTemplate
+ .item used by ItemTemplate and NewItemTemplate
+ .filter used by IndexTemplate
-# 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
+Templating works by instantiating one of the *Template classes above,
+passing in a handle to the cgi client, identifying the class and the
+template source directory.
-class TemplateFunctions:
- 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)
+The *Template class reads in the parsed template (parsing and caching
+as needed). When the render() method is called, the parse tree is
+traversed. Each node is either text (immediately output), a Require
+instance (resulting in a call to _test()), a Property instance (treated
+differently by .item and .index) or a Diplay instance (resulting in
+a call to one of the template_funcs.py functions).
- def do_plain(self, property, escape=0):
- ''' display a String property directly;
+In a .index list, Property tags are used to determine columns, and
+disappear before the actual rendering. Note that the template will
+be rendered many times in a .index.
- display a Date property in a specified time zone with an option to
- omit the time from the date stamp;
+In a .item, Property tags check if the node has the property.
- 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]')
- propclass = self.properties[property]
- if self.nodeid:
- # make sure the property is a valid one
- # TODO: this tests, but we should handle the exception
- prop_test = 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 = ''
- if isinstance(propclass, hyperdb.String):
- if value is None: value = ''
- else: value = str(value)
- elif isinstance(propclass, hyperdb.Password):
- if value is None: value = ''
- else: value = _('*encrypted*')
- elif isinstance(propclass, hyperdb.Date):
- 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]')
- elif isinstance(propclass, hyperdb.Multilink):
- linkcl = self.db.classes[propclass.classname]
- k = linkcl.labelprop()
- value = ', '.join([linkcl.get(i, k) for i in value])
+Templating is tested by the test_htmltemplate unit test suite. If you add
+a template function, add a test for all data types or the angry pink bunny
+will hunt you down.
+"""
+import weakref, os, types, cgi, sys, urllib, re, traceback
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+from template_parser import RoundupTemplate, Display, Property, Require
+from i18n import _
+import hyperdb, template_funcs
+
+MTIME = os.path.stat.ST_MTIME
+
+class MissingTemplateError(ValueError):
+ '''Error raised when a template file is missing
+ '''
+ pass
+
+# what a <require> tag results in
+def _test(attributes, client, classname, nodeid):
+ tests = {}
+ for nm, val in attributes:
+ tests[nm] = val
+ userid = client.db.user.lookup(client.user)
+ security = client.db.security
+ perms = tests.get('permission', None)
+ if perms:
+ del tests['permission']
+ perms = perms.split(',')
+ for value in perms:
+ if security.hasPermission(value, userid, classname):
+ # just passing the permission is OK
+ return 1
+ # try the attr conditions until one is met
+ if nodeid is None:
+ return 0
+ if not tests:
+ return 0
+ for propname, value in tests.items():
+ if value == '$userid':
+ tests[propname] = userid
+ return security.hasNodePermission(classname, nodeid, **tests)
+
+# what a <display> tag results in
+def _display(attributes, client, classname, cl, props, nodeid, filterspec=None):
+ call = attributes[0][1] #eg "field('prop2')"
+ pos = call.find('(')
+ funcnm = call[:pos]
+ func = templatefuncs.get(funcnm, None)
+ if func:
+ argstr = call[pos:]
+ args, kws = eval('splitargs'+argstr)
+ args = (client, classname, cl, props, nodeid, filterspec) + args
+ rslt = func(*args, **kws)
+ else:
+ rslt = _('no template function %s' % funcnm)
+ client.write(rslt)
+
+# what a <property> tag results in
+def _exists(attributes, cl, props, nodeid):
+ nm = attributes[0][1]
+ if nodeid:
+ return cl.get(nodeid, nm)
+ return props.get(nm, 0)
+
+class Template:
+ ''' base class of all templates.
+
+ knows how to compile & load a template.
+ knows how to render one item. '''
+ def __init__(self, client, templates, classname):
+ if isinstance(client, weakref.ProxyType):
+ self.client = client
else:
- s = _('Plain: bad propclass "%(propclass)s"')%locals()
- if escape:
- value = cgi.escape(value)
- return value
-
- 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)
+ self.client = weakref.proxy(client)
+ self.templatedir = templates
+ self.compiledtemplatedir = self.templatedir + 'c'
+ self.classname = classname
+ self.cl = self.client.db.getclass(self.classname)
+ self.properties = self.cl.getprops()
+ self.template = self._load()
+ self.filterspec = None
+ self.columns = None
+ self.nodeid = None
- def do_field(self, property, size=None, height=None, showid=0):
- ''' display a property like the plain displayer, but in a text field
- to be edited
- '''
- if not self.nodeid and self.form is None and self.filterspec is None:
- return _('[Field: not called from item]')
- propclass = self.properties[property]
- if (isinstance(propclass, hyperdb.Link) or
- isinstance(propclass, hyperdb.Multilink)):
- linkcl = self.db.classes[propclass.classname]
- def sortfunc(a, b, cl=linkcl):
- if cl.getprops().has_key('order'):
- sort_on = 'order'
- else:
- sort_on = cl.labelprop()
- r = cmp(cl.get(a, sort_on), cl.get(b, sort_on))
- return r
- 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 = []
- elif self.filterspec is not None:
- if isinstance(propclass, hyperdb.Multilink):
- value = self.filterspec.get(property, [])
- else:
- value = self.filterspec.get(property, '')
- else:
- # TODO: pull the value from the form
- if isinstance(propclass, hyperdb.Multilink): value = []
- else: value = ''
- 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 = '"'.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):
- l = ['<select name="%s">'%property]
- k = linkcl.labelprop()
- if value is None:
- s = 'selected '
- else:
- s = ''
- 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 == value:
- s = 'selected '
- if showid:
- lab = '%s%s: %s'%(propclass.classname, optionid, option)
- else:
- 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))
- l.append('</select>')
- s = '\n'.join(l)
- elif isinstance(propclass, hyperdb.Multilink):
- list = linkcl.list()
- list.sort(sortfunc)
- k = linkcl.labelprop()
- l = []
- # special treatment for nosy list
- if property == 'nosy':
- input_value = []
- else:
- input_value = value
- for v in value:
- lab = linkcl.get(v, k)
- if property != 'nosy':
- l.append('<a href="issue%s">%s: %s</a>'%(v,v,lab))
- else:
- input_value.append(lab)
- if size is None:
- size = '10'
- l.insert(0,'<input name="%s" size="%s" value="%s">'%(property,
- size, ','.join(input_value)))
- s = "<br>\n".join(l)
- else:
- s = _('Plain: bad propclass "%(propclass)s"')%locals()
- return s
+ def _load(self):
+ ''' Load a template from disk and parse it.
- def do_menu(self, property, size=None, height=None, showid=0):
- ''' for a Link property, display a menu of the available choices
+ Once parsed, the template is stored as a pickle in the
+ "htmlc" directory of the instance. If the file in there is
+ newer than the source template file, it's used in preference so
+ we don't have to re-parse.
'''
- propclass = self.properties[property]
- if self.nodeid:
- value = self.cl.get(self.nodeid, property)
+ # figure where the template source is
+ src = os.path.join(self.templatedir, self.classname + self.extension)
+
+ if not os.path.exists(src):
+ # hrm, nothing exactly matching what we're after, see if we can
+ # fall back on another template
+ if hasattr(self, 'fallbackextension'):
+ self.extension = self.fallbackextension
+ return self._load()
+ raise MissingTemplateError, self.classname + self.extension
+
+ # figure where the compiled template should be
+ cpl = os.path.join(self.compiledtemplatedir,
+ self.classname + self.extension)
+
+ if (not os.path.exists(cpl)
+ or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
+ # there's either no compiled template, or it's out of date
+ parser = RoundupTemplate()
+ parser.feed(open(src, 'r').read())
+ tmplt = parser.structure
+ try:
+ if not os.path.exists(self.compiledtemplatedir):
+ os.makedirs(self.compiledtemplatedir)
+ f = open(cpl, 'wb')
+ pickle.dump(tmplt, f)
+ f.close()
+ except Exception, e:
+ print "ouch in pickling: got a %s %r" % (e, e.args)
+ pass
else:
- # TODO: pull the value from the form
- if isinstance(propclass, hyperdb.Multilink): value = []
- else: value = None
- if isinstance(propclass, hyperdb.Link):
- linkcl = self.db.classes[propclass.classname]
- l = ['<select name="%s">'%property]
- k = linkcl.labelprop()
- s = ''
- if value is None:
- s = 'selected '
- l.append(_('<option %svalue="-1">- no selection -</option>')%s)
- 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:
- option = linkcl.get(optionid, k)
- s = ''
- if optionid in value:
- s = 'selected '
- if showid:
- lab = '%s%s: %s'%(propclass.classname, optionid, option)
- else:
- 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))
- l.append('</select>')
- return '\n'.join(l)
- return _('[Menu: not a link]')
+ # load the compiled template
+ f = open(cpl, 'rb')
+ tmplt = pickle.load(f)
+ return tmplt
- #XXX deviates from spec
- def do_link(self, property=None, is_download=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.
+ def _render(self, tmplt=None, test=_test, display=_display, exists=_exists):
+ ''' Render the template
'''
- if not self.nodeid and self.form is None:
- return _('[Link: not called from item]')
- 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)
- if is_download:
- return '<a href="%s%s/%s">%s</a>'%(linkname, value,
- linkvalue, linkvalue)
- else:
- return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
- if isinstance(propclass, hyperdb.Multilink):
- linkname = propclass.classname
- linkcl = self.db.classes[linkname]
- k = linkcl.labelprop()
- if not value:
- return _('[no %(propname)s]')%{'propname': property.capitalize()}
- l = []
- for value in value:
- linkvalue = linkcl.get(value, k)
- if is_download:
- l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
- linkvalue, linkvalue))
+ if tmplt is None:
+ tmplt = self.template
+
+ # go through the list of template "commands"
+ for entry in tmplt:
+ if isinstance(entry, type('')):
+ # string - just write it out
+ self.client.write(entry)
+
+ elif isinstance(entry, Require):
+ # a <require> tag
+ if test(entry.attributes, self.client, self.classname,
+ self.nodeid):
+ # require test passed, render the ok clause
+ self._render(entry.ok)
+ elif entry.fail:
+ # if there's a fail clause, render it
+ self._render(entry.fail)
+
+ elif isinstance(entry, Display):
+ # execute the <display> function
+ display(entry.attributes, self.client, self.classname,
+ self.cl, self.properties, self.nodeid, self.filterspec)
+
+ elif isinstance(entry, Property):
+ # do a <property> test
+ if self.columns is None:
+ # doing an Item - see if the property is present
+ if exists(entry.attributes, self.cl, self.properties,
+ self.nodeid):
+ self._render(entry.ok)
+ # XXX erm, should this be commented out?
+ #elif entry.attributes[0][1] in self.columns:
else:
- l.append('<a href="%s%s">%s</a>'%(linkname, value,
- linkvalue))
- return ', '.join(l)
- if isinstance(propclass, hyperdb.String) and value == '':
- return _('[no %(propname)s]')%{'propname': property.capitalize()}
- 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)
-
- 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]')
- propclass = self.properties[property]
- value = self.cl.get(self.nodeid, property)
- if isinstance(propclass, hyperdb.Multilink):
- return str(len(value))
- return _('[Count: not a Multilink]')
+ self._render(entry.ok)
- # 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").
+class IndexTemplate(Template):
+ ''' renders lists of items
- with the 'pretty' flag, make it pretty
- '''
- if not self.nodeid and self.form is None:
- return _('[Reldate: not called from item]')
- propclass = self.properties[property]
- if isinstance(not 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('.')
- if pretty:
- if not self.nodeid:
- return _('now')
- pretty = interval.pretty()
- if pretty is None:
- pretty = value.pretty()
- return pretty
- return str(interval)
-
- 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]')
-
-
- 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]')
-
- # get our current checkbox state
- if self.nodeid:
- # get the info from the node - make sure it's a list
- if isinstance(propclass, hyperdb.Link):
- value = [self.cl.get(self.nodeid, property)]
- else:
- value = self.cl.get(self.nodeid, property)
- elif self.filterspec is not None:
- # get the state from the filter specification (always a list)
- value = self.filterspec.get(property, [])
- else:
- # it's a new node, so there's no state
- value = []
-
- # so we can map to the linked node's "lable" property
- linkcl = self.db.classes[propclass.classname]
- l = []
- k = linkcl.labelprop()
- for optionid in linkcl.list():
- option = 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))
+ shows filter form (for new queries / to refine queries)
+ has clickable column headers (sort by this column / sort reversed)
+ has group by lines
+ has full text search match lines '''
+ extension = '.index'
- # 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)
-
- 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)
+ def __init__(self, client, templates, classname):
+ Template.__init__(self, client, templates, classname)
- # XXX new function
- def do_list(self, property, reverse=0):
- ''' list the items specified by property using the standard index for
- the class
+ def render(self, **kw):
+ ''' Render the template - well, wrap the rendering in a try/finally
+ so we're guaranteed to clean up after ourselves
'''
- propcl = self.properties[property]
- if not isinstance(propcl, hyperdb.Multilink):
- return _('[List: not a Multilink]')
- value = self.cl.get(self.nodeid, property)
- if reverse:
- value.reverse()
-
- # 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)
+ self.renderInner(**kw)
finally:
- self.client.write = write_save
-
- return fp.getvalue()
-
- # XXX new function
- def do_history(self, **args):
- ''' list the history of the item
+ self.cl = self.properties = self.client = None
+
+ def renderInner(self, filterspec={}, search_text='', filter=[], columns=[],
+ sort=[], group=[], show_display_form=1, nodeids=None,
+ show_customization=1, show_nodes=1, pagesize=50, startwith=0,
+ simple_search=1, xtracols=None):
+ ''' Take all the index arguments and render some HTML
'''
- if self.nodeid is None:
- 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):
- date_s = str(date).replace("."," ")
- l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
- date_s, user, action, args))
- l.append('</table>')
- return '\n'.join(l)
-
- # XXX new function
- def do_submit(self):
- ''' add a submit button for the item
- '''
- if self.nodeid:
- return _('<input type="submit" value="Submit Changes">')
- elif self.form is not None:
- return _('<input type="submit" value="Submit New Entry">')
- else:
- return _('[Submit: not called from item]')
-
-
-#
-# INDEX TEMPLATES
-#
-class IndexTemplateReplace:
- def __init__(self, globals, locals, props):
- self.globals = globals
- self.locals = locals
- self.props = props
-
- 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'))
- else:
- return ''
- if m.group('display'):
- command = m.group('command')
- return eval(command, self.globals, self.locals)
- print '*** unhandled match', m.groupdict()
-
-class IndexTemplate(TemplateFunctions):
- def __init__(self, client, templates, classname):
- 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()
-
- TemplateFunctions.__init__(self)
-
- 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
+ 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 = []
+ cl = self.cl
+ properties = self.properties
+ if xtracols is None:
+ xtracols = []
# XXX deviate from spec here ...
# load the index section template and figure the default columns from it
- template = open(os.path.join(self.templates,
- self.classname+'.index')).read()
- all_columns = self.col_re.findall(template)
+ displayable_props = []
+ all_columns = []
+ for node in self.template:
+ if isinstance(node, Property):
+ colnm = node.attributes[0][1]
+ if properties.has_key(colnm):
+ displayable_props.append(colnm)
+ all_columns.append(colnm)
+ elif colnm in xtracols:
+ all_columns.append(colnm)
if not columns:
- columns = []
- for name in all_columns:
- columns.append(name)
+ columns = all_columns
else:
- # re-sort columns to be the same order as all_columns
+ # re-sort columns to be the same order as displayable_props
l = []
for name in all_columns:
if name in columns:
l.append(name)
columns = l
+ self.columns = columns
+ # optimize the template
+ self.template = self._optimize(self.template)
+
# display the filter section
- if (show_display_form and
- self.instance.FILTER_POSITION in ('top and bottom', 'top')):
- w('<form action="index">\n')
- 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')
-
+ if (show_display_form and
+ self.client.instance.FILTER_POSITION.startswith('top')):
+ w('<form onSubmit="return submit_once()" action="%s">\n'%
+ self.client.classname)
+ self.filter_section(search_text, filter, columns, group,
+ displayable_props, sort, filterspec, pagesize, startwith,
+ simple_search)
# 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))
+ if show_display_form and not cname in xtracols:
+ sb = self.sortby(name, search_text, filterspec, columns, filter,
+ group, sort, pagesize)
+ anchor = "%s?%s"%(self.client.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')
# 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()
+ if show_nodes:
+ matches = None
+ if nodeids is None:
+ if search_text != '':
+ matches = self.client.db.indexer.search(
+ re.findall(r'\b\w{2,25}\b', search_text), cl)
+ nodeids = cl.filter(matches, filterspec, sort, group)
+ linecount = 0
+ for nodeid in nodeids[startwith:startwith+pagesize]:
+ # check for a group heading
+ if group_names:
+ this_group = [cl.get(nodeid, name, _('[no value]'))
+ 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 = self.client.db.getclass(prop.classname)
+ key = group_cl.getkey()
+ if key is None:
+ key = group_cl.labelprop()
+ value = cl.get(nodeid, name)
+ if value is None:
+ l.append(_('[unselected %(classname)s]')%{
+ 'classname': prop.classname})
+ else:
+ l.append(group_cl.get(value, key))
+ elif isinstance(prop, hyperdb.Multilink):
+ group_cl = self.client.db.getclass(prop.classname)
+ key = group_cl.getkey()
+ for value in cl.get(nodeid, name):
+ l.append(group_cl.get(value, key))
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
+ value = 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>\n'%(
+ len(columns), ', '.join(l)))
+ old_group = this_group
+
+ # display this node's row
+ self.nodeid = nodeid
+ self._render()
+ if matches:
+ self.node_matches(matches[nodeid], len(columns))
+ self.nodeid = None
- # display this node's row
- replace = IndexTemplateReplace(self.globals, locals(), columns)
- self.nodeid = nodeid
- w(replace.go(template))
- self.nodeid = None
-
- w('</table>')
+ w('</table>\n')
+ # the previous and next links
+ if nodeids:
+ baseurl = self.buildurl(filterspec, search_text, filter,
+ columns, sort, group, pagesize)
+ if startwith > 0:
+ prevurl = '<a href="%s&:startwith=%s"><< '\
+ 'Previous page</a>'%(baseurl, max(0, startwith-pagesize))
+ else:
+ prevurl = ""
+ if startwith + pagesize < len(nodeids):
+ nexturl = '<a href="%s&:startwith=%s">Next page '\
+ '>></a>'%(baseurl, startwith+pagesize)
+ else:
+ nexturl = ""
+ if prevurl or nexturl:
+ w('''<table width="100%%"><tr>
+ <td width="50%%" align="center">%s</td>
+ <td width="50%%" align="center">%s</td>
+ </tr></table>\n'''%(prevurl, nexturl))
# 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 action="index">\n')
- 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):
-
+ if (show_display_form and hasattr(self.client.instance,
+ 'FILTER_POSITION') and
+ self.client.instance.FILTER_POSITION.endswith('bottom')):
+ w('<form onSubmit="return submit_once()" action="%s">\n'%
+ self.client.classname)
+ self.filter_section(search_text, filter, columns, group,
+ displayable_props, sort, filterspec, pagesize, startwith,
+ simple_search)
+
+ def _optimize(self, tmplt):
+ columns = self.columns
+ t = []
+ for entry in tmplt:
+ if isinstance(entry, Property):
+ if entry.attributes[0][1] in columns:
+ t.extend(entry.ok)
+ else:
+ t.append(entry)
+ return t
+
+ def buildurl(self, filterspec, search_text, filter, columns, sort, group,
+ pagesize):
+ d = {'pagesize':pagesize, 'pagesize':pagesize,
+ 'classname':self.classname}
+ if search_text:
+ d['searchtext'] = 'search_text=%s&' % search_text
+ else:
+ d['searchtext'] = ''
+ d['filter'] = ','.join(map(urllib.quote,filter))
+ d['columns'] = ','.join(map(urllib.quote,columns))
+ d['sort'] = ','.join(map(urllib.quote,sort))
+ d['group'] = ','.join(map(urllib.quote,group))
+ tmp = []
+ for col, vals in filterspec.items():
+ vals = ','.join(map(urllib.quote,vals))
+ tmp.append('%s=%s' % (col, vals))
+ d['filters'] = '&'.join(tmp)
+ return ('%(classname)s?%(searchtext)s%(filters)s&:sort=%(sort)s&'
+ ':filter=%(filter)s&:group=%(group)s&:columns=%(columns)s&'
+ ':pagesize=%(pagesize)s'%d)
+
+ def node_matches(self, match, colspan):
+ ''' display the files and messages for a node that matched a
+ full text search
+ '''
w = self.client.write
+ db = self.client.db
+ message_links = []
+ file_links = []
+ if match.has_key('messages'):
+ for msgid in match['messages']:
+ k = db.msg.labelprop(1)
+ lab = db.msg.get(msgid, k)
+ msgpath = 'msg%s'%msgid
+ message_links.append('<a href="%(msgpath)s">%(lab)s</a>'
+ %locals())
+ w(_('<tr class="row-hilite"><td colspan="%s">'
+ ' Matched messages: %s</td></tr>\n')%(
+ colspan, ', '.join(message_links)))
+
+ if match.has_key('files'):
+ for fileid in match['files']:
+ filename = db.file.get(fileid, 'name')
+ filepath = 'file%s/%s'%(fileid, filename)
+ file_links.append('<a href="%(filepath)s">%(filename)s</a>'
+ %locals())
+ w(_('<tr class="row-hilite"><td colspan="%s">'
+ ' Matched files: %s</td></tr>\n')%(
+ colspan, ', '.join(file_links)))
+
+ def filter_form(self, search_text, filter, columns, group, all_columns,
+ sort, filterspec, pagesize):
+ sortspec = {}
+ for i in range(len(sort)):
+ mod = ''
+ colnm = sort[i]
+ if colnm[0] == '-':
+ mod = '-'
+ colnm = colnm[1:]
+ sortspec[colnm] = '%d%s' % (i+1, mod)
+
+ startwith = 0
+ rslt = []
+ w = rslt.append
- # 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>')
+ # display the filter section
+ w( '<br>')
+ w( '<table border=0 cellspacing=0 cellpadding=1>')
+ w( '<tr class="list-header">')
+ w(_(' <th align="left" colspan="7">Filter specification...</th>'))
+ w( '</tr>')
+ # see if we have any indexed properties
+ if self.client.classname in self.client.db.config.HEADER_SEARCH_LINKS:
w('<tr class="location-bar">')
- w(_(' <th align="left" colspan="2">Filter specification...</th>'))
+ w(' <td align="right" class="form-label"><b>Search Terms</b></td>')
+ w(' <td colspan=6 class="form-text"> '
+ '<input type="text"name="search_text" value="%s" size="50">'
+ '</td>'%search_text)
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 = []
- for name in self.properties.keys():
- if name in all_filters or name in all_columns:
- names.append(name)
- 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>'
- '<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 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:
- 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')
+ w( '<tr class="location-bar">')
+ w( ' <th align="center" width="20%"> </th>')
+ w(_(' <th align="center" width="10%">Show</th>'))
+ w(_(' <th align="center" width="10%">Group</th>'))
+ w(_(' <th align="center" width="10%">Sort</th>'))
+ w(_(' <th colspan="3" align="center">Condition</th>'))
+ w( '</tr>')
+
+ properties = self.client.db.getclass(self.classname).getprops()
+ all_columns = properties.keys()
+ all_columns.sort()
+ for nm in all_columns:
+ propdescr = properties.get(nm, None)
+ if not propdescr:
+ print "hey sysadmin - %s is not a property of %r" % (nm, self.classname)
+ continue
+ w( '<tr class="location-bar">')
+ w(_(' <td align="right" class="form-label"><b>%s</b></td>' % nm.capitalize()))
+ # show column - can't show multilinks
+ if isinstance(propdescr, hyperdb.Multilink):
+ w(' <td></td>')
+ else:
+ checked = columns and nm in columns or 0
+ checked = ('', 'checked')[checked]
+ w(' <td align="center" class="form-text"><input type="checkbox" name=":columns"'
+ 'value="%s" %s></td>' % (nm, checked) )
+ # can only group on Link
+ if isinstance(propdescr, hyperdb.Link):
+ checked = group and nm in group or 0
+ checked = ('', 'checked')[checked]
+ w(' <td align="center" class="form-text"><input type="checkbox" name=":group"'
+ 'value="%s" %s></td>' % (nm, checked) )
+ else:
+ w(' <td></td>')
+ # sort - no sort on Multilinks
+ if isinstance(propdescr, hyperdb.Multilink):
+ w('<td></td>')
+ else:
+ val = sortspec.get(nm, '')
+ w('<td align="center" class="form-text"><input type="text" name=":%s_ss" size="3"'
+ 'value="%s"></td>' % (nm,val))
+ # condition
+ val = ''
+ if isinstance(propdescr, hyperdb.Link):
+ op = "is in "
+ xtra = '<a href="javascript:help_window(\'classhelp?classname=%s&properties=id,%s\', \'200\', \'400\')"><b>(list)</b></a>' \
+ % (propdescr.classname, self.client.db.getclass(propdescr.classname).labelprop())
+ val = ','.join(filterspec.get(nm, ''))
+ elif isinstance(propdescr, hyperdb.Multilink):
+ op = "contains "
+ xtra = '<a href="javascript:help_window(\'classhelp?classname=%s&properties=id,%s\', \'200\', \'400\')"><b>(list)</b></a>' \
+ % (propdescr.classname, self.client.db.getclass(propdescr.classname).labelprop())
+ val = ','.join(filterspec.get(nm, ''))
+ elif isinstance(propdescr, hyperdb.String) and nm != 'id':
+ op = "equals "
+ xtra = ""
+ val = filterspec.get(nm, '')
+ elif isinstance(propdescr, hyperdb.Boolean):
+ op = "is "
+ xtra = ""
+ val = filterspec.get(nm, None)
+ if val is not None:
+ val = 'True' and val or 'False'
+ else:
+ val = ''
+ elif isinstance(propdescr, hyperdb.Number):
+ op = "equals "
+ xtra = ""
+ val = str(filterspec.get(nm, ''))
+ else:
+ w('<td></td><td></td><td></td></tr>')
+ continue
+ checked = filter and nm in filter or 0
+ checked = ('', 'checked')[checked]
+ w( ' <td class="form-text"><input type="checkbox" name=":filter" value="%s" %s></td>' \
+ % (nm, checked))
+ w(_(' <td class="form-label" nowrap>%s</td><td class="form-text" nowrap>'
+ '<input type="text" name=":%s_fs" value="%s" size=50>%s</td>' % (op, nm, val, xtra)))
+ w( '</tr>')
+ w('<tr class="location-bar">')
+ w(' <td colspan=7><hr></td>')
+ w('</tr>')
+ w('<tr class="location-bar">')
+ w(_(' <td align="right" class="form-label">Pagesize</td>'))
+ w(' <td colspan=2 align="center" class="form-text"><input type="text" name=":pagesize"'
+ 'size="3" value="%s"></td>' % pagesize)
+ w(' <td colspan=4></td>')
+ w('</tr>')
+ w('<tr class="location-bar">')
+ w(_(' <td align="right" class="form-label">Start With</td>'))
+ w(' <td colspan=2 align="center" class="form-text"><input type="text" name=":startwith"'
+ 'size="3" value="%s"></td>' % startwith)
+ w(' <td colspan=3></td>')
+ w(' <td></td>')
+ w('</tr>')
+ w('<input type=hidden name=":advancedsearch" value="1">')
+
+ return '\n'.join(rslt)
+
+ def simple_filter_form(self, search_text, filter, columns, group, all_columns,
+ sort, filterspec, pagesize):
+
+ startwith = 0
+ rslt = []
+ w = rslt.append
- # Grouping
- w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
- for name in names:
- prop = self.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')
+ # display the filter section
+ w( '<br>')
+ w( '<table border=0 cellspacing=0 cellpadding=1>')
+ w( '<tr class="list-header">')
+ w(_(' <th align="left" colspan="7">Query modifications...</th>'))
+ w( '</tr>')
- 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')
+ if group:
+ selectedgroup = group[0]
+ groupopts = ['<select name=":group">','<option value="">--no selection--</option>']
+ else:
+ selectedgroup = None
+ groupopts = ['<select name=":group">','<option value="" selected>--no selection--</option>']
+ descending = 0
+ if sort:
+ selectedsort = sort[0]
+ if selectedsort[0] == '-':
+ selectedsort = selectedsort[1:]
+ descending = 1
+ sortopts = ['<select name=":sort">', '<option value="">--no selection--</option>']
+ else:
+ selectedsort = None
+ sortopts = ['<select name=":sort">', '<option value="" selected>--no selection--</option>']
+
+ for nm in all_columns:
+ propdescr = self.client.db.getclass(self.client.classname).getprops().get(nm, None)
+ if not propdescr:
+ print "hey sysadmin - %s is not a property of %r" % (nm, self.classname)
+ continue
+ if isinstance(propdescr, hyperdb.Link):
+ selected = ''
+ if nm == selectedgroup:
+ selected = 'selected'
+ groupopts.append('<option value="%s" %s>%s</option>' % (nm, selected, nm.capitalize()))
+ selected = ''
+ if nm == selectedsort:
+ selected = 'selected'
+ sortopts.append('<option value="%s" %s>%s</option>' % (nm, selected, nm.capitalize()))
+ if len(groupopts) > 2:
+ groupopts.append('</select>')
+ groupopts = '\n'.join(groupopts)
+ w('<tr class="location-bar">')
+ w(' <td align="right" class="form-label"><b>Group</b></td>')
+ w(' <td class="form-text">%s</td>' % groupopts)
+ w('</tr>')
+ if len(sortopts) > 2:
+ sortopts.append('</select>')
+ sortopts = '\n'.join(sortopts)
+ w('<tr class="location-bar">')
+ w(' <td align="right" class="form-label"><b>Sort</b></td>')
+ checked = descending and 'checked' or ''
+ w(' <td class="form-text">%s <span class="form-label">Descending</span>'
+ '<input type=checkbox name=":descending" value="1" %s></td>' % (sortopts, checked))
+ w('</tr>')
+ w('<input type=hidden name="search_text" value="%s">' % urllib.quote(search_text))
+ w('<input type=hidden name=":filter" value="%s">' % ','.join(filter))
+ w('<input type=hidden name=":columns" value="%s">' % ','.join(columns))
+ for nm in filterspec.keys():
+ w('<input type=hidden name=":%s_fs" value="%s">' % (nm, ','.join(filterspec[nm])))
+ w('<input type=hidden name=":pagesize" value="%s">' % pagesize)
+
+ return '\n'.join(rslt)
+
+ def filter_section(self, search_text, filter, columns, group, all_columns,
+ sort, filterspec, pagesize, startwith, simpleform=1):
+ w = self.client.write
+ if simpleform:
+ w(self.simple_filter_form(search_text, filter, columns, group,
+ all_columns, sort, filterspec, pagesize))
+ else:
+ w(self.filter_form(search_text, filter, columns, group, all_columns,
+ sort, filterspec, pagesize))
+ w(' <tr class="location-bar">\n')
+ w(' <td colspan=7><hr></td>\n')
+ w(' </tr>\n')
+ w(' <tr class="location-bar">\n')
+ w(' <td> </td>\n')
+ w(' <td colspan=6><input type="submit" name="Query" value="Redisplay"></td>\n')
+ w(' </tr>\n')
+ if (not simpleform
+ and self.client.db.getclass('user').getprops().has_key('queries')
+ and not self.client.user in (None, "anonymous")):
+ w(' <tr class="location-bar">\n')
+ w(' <td colspan=7><hr></td>\n')
+ w(' </tr>\n')
+ w(' <tr class="location-bar">\n')
+ w(' <td align=right class="form-label">Name</td>\n')
+ w(' <td colspan=2 class="form-text"><input type="text" name=":name" value=""></td>\n')
+ w(' <td colspan=4 rowspan=2 class="form-help">If you give the query a name '
+ 'and click <b>Save</b>, it will appear on your menu. Saved queries may be '
+ 'edited by going to <b>My Details</b> and clicking on the query name.</td>')
+ w(' </tr>\n')
+ w(' <tr class="location-bar">\n')
+ w(' <td> </td><input type="hidden" name=":classname" value="%s">\n' % self.classname)
+ w(' <td colspan=2><input type="submit" name="Query" value="Save"></td>\n')
+ w(' </tr>\n')
w('</table>\n')
- # and the outer table
- w('</td></tr></table>')
-
-
- def sortby(self, sort_name, filterspec, columns, filter, group, sort):
+ def sortby(self, sort_name, search_text, filterspec, columns, filter,
+ group, sort, pagesize):
+ ''' Figure the link for a column heading so we can sort by that
+ column
+ '''
l = []
w = l.append
+ if search_text:
+ w('search_text=%s' % search_text)
for k, v in filterspec.items():
k = urllib.quote(k)
if type(v) == type([]):
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:
+ w(':pagesize=%s' % pagesize)
+ w(':startwith=0')
+
+ # handle the sorting - if we're already sorting by this column,
+ # then reverse the sorting, otherwise set the sorting to be this
+ # column only
+ sorting = None
+ if len(sort) == 1:
+ name = sort[0]
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:
- def __init__(self, globals, locals, cl, nodeid):
- self.globals = globals
- self.locals = locals
- self.cl = cl
- self.nodeid = nodeid
-
- 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 self.nodeid and self.cl.get(self.nodeid, m.group('name')):
- replace = ItemTemplateReplace(self.globals, {}, self.cl,
- self.nodeid)
- return replace.go(m.group('text'))
- else:
- return ''
- if m.group('display'):
- command = m.group('command')
- return eval(command, self.globals, self.locals)
- print '*** unhandled match', m.groupdict()
+ if dir == '-' and name[1:] == sort_name:
+ sorting = ':sort=%s'%sort_name
+ elif name == sort_name:
+ sorting = ':sort=-%s'%sort_name
+ if sorting is None:
+ sorting = ':sort=%s'%sort_name
+ w(sorting)
+ return '&'.join(l)
-class ItemTemplate(TemplateFunctions):
+class ItemTemplate(Template):
+ ''' show one node as a form '''
+ extension = '.item'
def __init__(self, client, templates, classname):
- 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()
-
- TemplateFunctions.__init__(self)
-
+ Template.__init__(self, client, templates, classname)
+ self.nodeid = client.nodeid
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 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>')
-
+ try:
+ cl = self.cl
+ properties = self.properties
+ 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 = self.client.write
+ w('<form onSubmit="return submit_once()" action="%s%s" '
+ 'method="POST" enctype="multipart/form-data">'%(self.classname,
+ nodeid))
+ try:
+ self._render()
+ except:
+ # make sure we don't commit any changes
+ self.client.db.rollback()
+ s = StringIO.StringIO()
+ traceback.print_exc(None, s)
+ w('<pre class="system-msg">%s</pre>'%cgi.escape(s.getvalue()))
+ w('</form>')
+ finally:
+ self.cl = self.properties = self.client = None
-class NewItemTemplate(TemplateFunctions):
+class NewItemTemplate(Template):
+ ''' display a form for creating a new node '''
+ extension = '.newitem'
+ fallbackextension = '.item'
def __init__(self, client, templates, classname):
- 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()
-
- TemplateFunctions.__init__(self)
-
+ Template.__init__(self, client, templates, classname)
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 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>')
+ self.form = form
+ w = self.client.write
+ c = self.client.classname
+ 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))
+ self._render()
+ w('</form>')
+ finally:
+ self.cl = self.properties = self.client = None
+
+def splitargs(*args, **kws):
+ return args, kws
+# [('permission', 'perm2,perm3'), ('assignedto', '$userid'), ('status', 'open')]
+
+templatefuncs = {}
+for nm in template_funcs.__dict__.keys():
+ if nm.startswith('do_'):
+ templatefuncs[nm[3:]] = getattr(template_funcs, nm)
#
# $Log: not supported by cvs2svn $
+# Revision 1.111 2002/08/15 00:40:10 richard
+# cleanup
+#
+# Revision 1.110 2002/08/13 20:16:09 gmcm
+# Use a real parser for templates.
+# Rewrite htmltemplate to use the parser (hack, hack).
+# Move the "do_XXX" methods to template_funcs.py.
+# Redo the funcion tests (but not Template tests - they're hopeless).
+# Simplified query form in cgi_client.
+# Ability to delete msgs, files, queries.
+# Ability to edit the metadata on files.
+#
+# Revision 1.109 2002/08/01 15:06:08 gmcm
+# Use same regex to split search terms as used to index text.
+# Fix to back_metakit for not changing journaltag on reopen.
+# Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
+# Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
+#
+# Revision 1.108 2002/07/31 22:40:50 gmcm
+# Fixes to the search form and saving queries.
+# Fixes to sorting in back_metakit.py.
+#
+# Revision 1.107 2002/07/30 05:27:30 richard
+# nicer error messages, and a bugfix
+#
+# Revision 1.106 2002/07/30 02:41:04 richard
+# Removed the confusing, ugly two-column sorting stuff. Column heading clicks
+# now only sort on one column. Nice and simple and obvious.
+#
+# Revision 1.105 2002/07/26 08:26:59 richard
+# Very close now. The cgi and mailgw now use the new security API. The two
+# templates have been migrated to that setup. Lots of unit tests. Still some
+# issue in the web form for editing Roles assigned to users.
+#
+# Revision 1.104 2002/07/25 07:14:05 richard
+# Bugger it. Here's the current shape of the new security implementation.
+# Still to do:
+# . call the security funcs from cgi and mailgw
+# . change shipped templates to include correct initialisation and remove
+# the old config vars
+# ... that seems like a lot. The bulk of the work has been done though. Honest :)
+#
+# Revision 1.103 2002/07/20 19:29:10 gmcm
+# Fixes/improvements to the search form & saved queries.
+#
+# Revision 1.102 2002/07/18 23:07:08 richard
+# Unit tests and a few fixes.
+#
+# Revision 1.101 2002/07/18 11:17:30 gmcm
+# Add Number and Boolean types to hyperdb.
+# Add conversion cases to web, mail & admin interfaces.
+# Add storage/serialization cases to back_anydbm & back_metakit.
+#
+# Revision 1.100 2002/07/18 07:01:54 richard
+# minor bugfix
+#
+# Revision 1.99 2002/07/17 12:39:10 gmcm
+# Saving, running & editing queries.
+#
+# Revision 1.98 2002/07/10 00:17:46 richard
+# . added sorting of checklist HTML display
+#
+# Revision 1.97 2002/07/09 05:20:09 richard
+# . added email display function - mangles email addrs so they're not so easily
+# scraped from the web
+#
+# Revision 1.96 2002/07/09 04:19:09 richard
+# Added reindex command to roundup-admin.
+# Fixed reindex on first access.
+# Also fixed reindexing of entries that change.
+#
+# Revision 1.95 2002/07/08 15:32:06 gmcm
+# Pagination of index pages.
+# New search form.
+#
+# Revision 1.94 2002/06/27 15:38:53 gmcm
+# Fix the cycles (a clear method, called after render, that removes
+# the bound methods from the globals dict).
+# Use cl.filter instead of cl.list followed by sortfunc. For some
+# backends (Metakit), filter can sort at C speeds, cutting >10 secs
+# off of filling in the <select...> box for assigned_to when you
+# have 600+ users.
+#
+# Revision 1.93 2002/06/27 12:05:25 gmcm
+# Default labelprops to id.
+# In history, make sure there's a .item before making a link / multilink into an href.
+# Also in history, cgi.escape String properties.
+# Clean up some of the reference cycles.
+#
+# Revision 1.92 2002/06/11 04:57:04 richard
+# Added optional additional property to display in a Multilink form menu.
+#
+# Revision 1.91 2002/05/31 00:08:02 richard
+# can now just display a link/multilink id - useful for stylesheet stuff
+#
+# Revision 1.90 2002/05/25 07:16:24 rochecompaan
+# Merged search_indexing-branch with HEAD
+#
+# Revision 1.89 2002/05/15 06:34:47 richard
+# forgot to fix the templating for last change
+#
+# 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.2.2 2002/04/20 13:23:32 rochecompaan
+# We now have a separate search page for nodes. Search links for
+# different classes can be customized in instance_config similar to
+# index links.
+#
+# Revision 1.84.2.1 2002/04/19 19:54:42 rochecompaan
+# cgi_client.py
+# removed search link for the time being
+# moved rendering of matches to htmltemplate
+# hyperdb.py
+# filtering of nodes on full text search incorporated in filter method
+# roundupdb.py
+# added paramater to call of filter method
+# roundup_indexer.py
+# added search method to RoundupIndexer class
+#
+# 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 ?
#
#
# vim: set filetype=python ts=4 sw=4 et si
+