Code

no idea why this code existed, but bye bye
[roundup.git] / roundup / cgi / templating.py
index 2a04f5ab44ea1eba69522dccd351f10bd4587ae7..2f2a478f14e570dbe49dc0cae1c0b21f67dfb6a5 100644 (file)
-import sys, cgi, urllib, os, re
+import sys, cgi, urllib, os, re, os.path, time, errno
 
 from roundup import hyperdb, date
 from roundup.i18n import _
 
-
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
 try:
     import StructuredText
 except ImportError:
     StructuredText = None
 
-# Make sure these modules are loaded
-# I need these to run PageTemplates outside of Zope :(
-# If we're running in a Zope environment, these modules will be loaded
-# already...
-if not sys.modules.has_key('zLOG'):
-    import zLOG
-    sys.modules['zLOG'] = zLOG
-if not sys.modules.has_key('MultiMapping'):
-    import MultiMapping
-    sys.modules['MultiMapping'] = MultiMapping
-if not sys.modules.has_key('ComputedAttribute'):
-    import ComputedAttribute
-    sys.modules['ComputedAttribute'] = ComputedAttribute
-if not sys.modules.has_key('ExtensionClass'):
-    import ExtensionClass
-    sys.modules['ExtensionClass'] = ExtensionClass
-if not sys.modules.has_key('Acquisition'):
-    import Acquisition
-    sys.modules['Acquisition'] = Acquisition
-
-# now it's safe to import PageTemplates and ZTUtils
-from PageTemplates import PageTemplate
-import ZTUtils
+# bring in the templating support
+from roundup.cgi.PageTemplates import PageTemplate
+from roundup.cgi.PageTemplates.Expressions import getEngine
+from roundup.cgi.TAL.TALInterpreter import TALInterpreter
+from roundup.cgi import ZTUtils
+
+# XXX WAH pagetemplates aren't pickleable :(
+#def getTemplate(dir, name, classname=None, request=None):
+#    ''' Interface to get a template, possibly loading a compiled template.
+#    '''
+#    # source
+#    src = os.path.join(dir, name)
+#
+#    # see if we can get a compile from the template"c" directory (most
+#    # likely is "htmlc"
+#    split = list(os.path.split(dir))
+#    split[-1] = split[-1] + 'c'
+#    cdir = os.path.join(*split)
+#    split.append(name)
+#    cpl = os.path.join(*split)
+#
+#    # ok, now see if the source is newer than the compiled (or if the
+#    # compiled even exists)
+#    MTIME = os.path.stat.ST_MTIME
+#    if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
+#        # nope, we need to compile
+#        pt = RoundupPageTemplate()
+#        pt.write(open(src).read())
+#        pt.id = name
+#
+#        # save off the compiled template
+#        if not os.path.exists(cdir):
+#            os.makedirs(cdir)
+#        f = open(cpl, 'wb')
+#        pickle.dump(pt, f)
+#        f.close()
+#    else:
+#        # yay, use the compiled template
+#        f = open(cpl, 'rb')
+#        pt = pickle.load(f)
+#    return pt
+
+templates = {}
+
+class NoTemplate(Exception):
+    pass
+
+def getTemplate(dir, name, extension, classname=None, request=None):
+    ''' Interface to get a template, possibly loading a compiled template.
+
+        "name" and "extension" indicate the template we're after, which in
+        most cases will be "name.extension". If "extension" is None, then
+        we look for a template just called "name" with no extension.
+
+        If the file "name.extension" doesn't exist, we look for
+        "_generic.extension" as a fallback.
+    '''
+    # default the name to "home"
+    if name is None:
+        name = 'home'
+
+    # find the source, figure the time it was last modified
+    if extension:
+        filename = '%s.%s'%(name, extension)
+    else:
+        filename = name
+    src = os.path.join(dir, filename)
+    try:
+        stime = os.stat(src)[os.path.stat.ST_MTIME]
+    except os.error, error:
+        if error.errno != errno.ENOENT:
+            raise
+        if not extension:
+            raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
+
+        # try for a generic template
+        generic = '_generic.%s'%extension
+        src = os.path.join(dir, generic)
+        try:
+            stime = os.stat(src)[os.path.stat.ST_MTIME]
+        except os.error, error:
+            if error.errno != errno.ENOENT:
+                raise
+            # nicer error
+            raise NoTemplate, 'No template file exists for templating '\
+                '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
+                extension, filename, generic)
+        filename = generic
+
+    key = (dir, filename)
+    if templates.has_key(key) and stime < templates[key].mtime:
+        # compiled template is up to date
+        return templates[key]
+
+    # compile the template
+    templates[key] = pt = RoundupPageTemplate()
+    pt.write(open(src).read())
+    pt.id = filename
+    pt.mtime = time.time()
+    return pt
 
 class RoundupPageTemplate(PageTemplate.PageTemplate):
     ''' A Roundup-specific PageTemplate.
@@ -39,26 +123,13 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
         Interrogate the client to set up the various template variables to
         be available:
 
-        *class*
-          The current class of node being displayed as an HTMLClass
-          instance.
-        *item*
-          The current node from the database, if we're viewing a specific
-          node, as an HTMLItem instance. If it doesn't exist, then we're
-          on a new item page.
-        (*classname*)
-          this is one of two things:
-
-          1. the *item* is also available under its classname, so a *user*
-             node would also be available under the name *user*. This is
-             also an HTMLItem instance.
-          2. if there's no *item* then the current class is available
-             through this name, thus "user/name" and "user/name/menu" will
-             still work - the latter will pull information from the form
-             if it can.
-        *form*
-          The current CGI form information as a mapping of form argument
-          name to value
+        *context*
+         this is one of three things:
+         1. None - we're viewing a "home" page
+         2. The current class of item being displayed. This is an HTMLClass
+            instance.
+         3. The current item from the database, if we're viewing a specific
+            item, as an HTMLItem instance.
         *request*
           Includes information about the current request, including:
            - the url
@@ -66,94 +137,160 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
              ``properties``, etc) parsed out of the form. 
            - methods for easy filterspec link generation
            - *user*, the current user node as an HTMLItem instance
+           - *form*, the current CGI form information as a FieldStorage
         *instance*
           The current instance
         *db*
           The current database, through which db.config may be reached.
-
-        Maybe also:
-
-        *modules*
-          python modules made available (XXX: not sure what's actually in
-          there tho)
     '''
-    def __init__(self, client, classname=None, request=None):
-        ''' Extract the vars from the client and install in the context.
-        '''
-        self.client = client
-        self.classname = classname or self.client.classname
-        self.request = request or HTMLRequest(self.client)
-
-    def pt_getContext(self):
+    def getContext(self, client, classname, request):
         c = {
-             'klass': HTMLClass(self.client, self.classname),
              'options': {},
              'nothing': None,
-             'request': self.request,
-             'content': self.client.content,
-             'db': HTMLDatabase(self.client),
-             'instance': self.client.instance
+             'request': request,
+             'content': client.content,
+             'db': HTMLDatabase(client),
+             'instance': client.instance,
+             'utils': TemplatingUtils(client),
         }
         # add in the item if there is one
-        if self.client.nodeid:
-            c['item'] = HTMLItem(self.client.db, self.classname,
-                self.client.nodeid)
-            c[self.classname] = c['item']
+        if client.nodeid:
+            if classname == 'user':
+                c['context'] = HTMLUser(client, classname, client.nodeid)
+            else:
+                c['context'] = HTMLItem(client, classname, client.nodeid)
         else:
-            c[self.classname] = c['klass']
+            c['context'] = HTMLClass(client, classname)
         return c
-   
-    def render(self, *args, **kwargs):
-        if not kwargs.has_key('args'):
-            kwargs['args'] = args
-        return self.pt_render(extra_context={'options': kwargs})
+
+    def render(self, client, classname, request, **options):
+        """Render this Page Template"""
+
+        if not self._v_cooked:
+            self._cook()
+
+        __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
+
+        if self._v_errors:
+            raise PageTemplate.PTRuntimeError, \
+                'Page Template %s has errors.'%self.id
+
+        # figure the context
+        classname = classname or client.classname
+        request = request or HTMLRequest(client)
+        c = self.getContext(client, classname, request)
+        c.update({'options': options})
+
+        # and go
+        output = StringIO.StringIO()
+        TALInterpreter(self._v_program, self._v_macros,
+            getEngine().getContext(c), output, tal=1, strictinsert=0)()
+        return output.getvalue()
 
 class HTMLDatabase:
     ''' Return HTMLClasses for valid class fetches
     '''
     def __init__(self, client):
-        self.client = client
+        self._client = client
+
+        # we want config to be exposed
         self.config = client.db.config
+
+    def __getitem__(self, item):
+        self._client.db.getclass(item)
+        return HTMLClass(self._client, item)
+
     def __getattr__(self, attr):
-        self.client.db.getclass(attr)
-        return HTMLClass(self.client, attr)
+        try:
+            return self[attr]
+        except KeyError:
+            raise AttributeError, attr
+
     def classes(self):
-        l = self.client.db.classes.keys()
+        l = self._client.db.classes.keys()
         l.sort()
-        return [HTMLClass(self.client, cn) for cn in l]
-        
-class HTMLClass:
+        return [HTMLClass(self._client, cn) for cn in l]
+
+def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
+    cl = db.getclass(prop.classname)
+    l = []
+    for entry in ids:
+        if num_re.match(entry):
+            l.append(entry)
+        else:
+            l.append(cl.lookup(entry))
+    return l
+
+class HTMLPermissions:
+    ''' Helpers that provide answers to commonly asked Permission questions.
+    '''
+    def is_edit_ok(self):
+        ''' Is the user allowed to Edit the current class?
+        '''
+        return self._db.security.hasPermission('Edit', self._client.userid,
+            self._classname)
+    def is_view_ok(self):
+        ''' Is the user allowed to View the current class?
+        '''
+        return self._db.security.hasPermission('View', self._client.userid,
+            self._classname)
+    def is_only_view_ok(self):
+        ''' Is the user only allowed to View (ie. not Edit) the current class?
+        '''
+        return self.is_view_ok() and not self.is_edit_ok()
+
+class HTMLClass(HTMLPermissions):
     ''' Accesses through a class (either through *class* or *db.<classname>*)
     '''
     def __init__(self, client, classname):
-        self.client = client
-        self.db = client.db
-        self.classname = classname
+        self._client = client
+        self._db = client.db
+
+        # we want classname to be exposed, but _classname gives a
+        # consistent API for extending Class/Item
+        self._classname = self.classname = classname
         if classname is not None:
-            self.klass = self.db.getclass(self.classname)
-            self.props = self.klass.getprops()
+            self._klass = self._db.getclass(self.classname)
+            self._props = self._klass.getprops()
 
     def __repr__(self):
         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
 
     def __getitem__(self, item):
-        ''' return an HTMLItem instance'''
-        #print 'getitem', (self, attr)
-        if item == 'creator':
-            return HTMLUser(self.client)
+        ''' return an HTMLProperty instance
+        '''
+       #print 'HTMLClass.getitem', (self, item)
+
+        # we don't exist
+        if item == 'id':
+            return None
 
-        if not self.props.has_key(item):
-            raise KeyError, item
-        prop = self.props[item]
+        # get the property
+        prop = self._props[item]
 
         # look up the correct HTMLProperty class
+        form = self._client.form
         for klass, htmlklass in propclasses:
-            if isinstance(prop, hyperdb.Multilink):
-                value = []
+            if not isinstance(prop, klass):
+                continue
+            if form.has_key(item):
+                if isinstance(prop, hyperdb.Multilink):
+                    value = lookupIds(self._db, prop,
+                        handleListCGIValue(form[item]))
+                elif isinstance(prop, hyperdb.Link):
+                    value = form[item].value.strip()
+                    if value:
+                        value = lookupIds(self._db, prop, [value])[0]
+                    else:
+                        value = None
+                else:
+                    value = form[item].value.strip() or None
             else:
-                value = None
-            if isinstance(prop, klass):
-                return htmlklass(self.db, '', prop, item, value)
+                if isinstance(prop, hyperdb.Multilink):
+                    value = []
+                else:
+                    value = None
+            return htmlklass(self._client, '', prop, item, value)
 
         # no good
         raise KeyError, item
@@ -166,23 +303,69 @@ class HTMLClass:
             raise AttributeError, attr
 
     def properties(self):
-        ''' Return HTMLProperty for all props
+        ''' Return HTMLProperty for all of this class' properties.
         '''
         l = []
-        for name, prop in self.props.items():
+        for name, prop in self._props.items():
             for klass, htmlklass in propclasses:
                 if isinstance(prop, hyperdb.Multilink):
                     value = []
                 else:
                     value = None
                 if isinstance(prop, klass):
-                    l.append(htmlklass(self.db, '', prop, name, value))
+                    l.append(htmlklass(self._client, '', prop, name, value))
         return l
 
     def list(self):
-        l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
+        ''' List all items in this class.
+        '''
+        if self.classname == 'user':
+            klass = HTMLUser
+        else:
+            klass = HTMLItem
+
+        # get the list and sort it nicely
+        l = self._klass.list()
+        sortfunc = make_sort_function(self._db, self._prop.classname)
+        l.sort(sortfunc)
+
+        l = [klass(self._client, self.classname, x) for x in l]
         return l
 
+    def csv(self):
+        ''' Return the items of this class as a chunk of CSV text.
+        '''
+        # get the CSV module
+        try:
+            import csv
+        except ImportError:
+            return 'Sorry, you need the csv module to use this function.\n'\
+                'Get it from: http://www.object-craft.com.au/projects/csv/'
+
+        props = self.propnames()
+        p = csv.parser()
+        s = StringIO.StringIO()
+        s.write(p.join(props) + '\n')
+        for nodeid in self._klass.list():
+            l = []
+            for name in props:
+                value = self._klass.get(nodeid, name)
+                if value is None:
+                    l.append('')
+                elif isinstance(value, type([])):
+                    l.append(':'.join(map(str, value)))
+                else:
+                    l.append(str(self._klass.get(nodeid, name)))
+            s.write(p.join(l) + '\n')
+        return s.getvalue()
+
+    def propnames(self):
+        ''' Return the list of the names of the properties of this class.
+        '''
+        idlessprops = self._klass.getprops(protected=0).keys()
+        idlessprops.sort()
+        return ['id'] + idlessprops
+
     def filter(self, request=None):
         ''' Return a list of items from this class, filtered and sorted
             by the current requested filterspec/filter/sort/group args
@@ -191,24 +374,35 @@ class HTMLClass:
             filterspec = request.filterspec
             sort = request.sort
             group = request.group
-        l = [HTMLItem(self.db, self.classname, x)
-             for x in self.klass.filter(None, filterspec, sort, group)]
+        if self.classname == 'user':
+            klass = HTMLUser
+        else:
+            klass = HTMLItem
+        l = [klass(self._client, self.classname, x)
+             for x in self._klass.filter(None, filterspec, sort, group)]
         return l
 
-    def classhelp(self, properties, label='?', width='400', height='400'):
-        '''pop up a javascript window with class help
+    def classhelp(self, properties=None, label='list', width='500',
+            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').
+            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'). Properties defaults to all the
+            properties of a class (excluding id, creator, created and
+            activity).
 
-           You may optionally override the label displayed, the width and
-           height. The popup window will be resizable and scrollable.
+            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>'%(self.classname,
-            properties, width, height, label)
+        if properties is None:
+            properties = self._klass.getprops(protected=0).keys()
+            properties.sort()
+            properties = ','.join(properties)
+        return '<a href="javascript:help_window(\'%s?:template=help&' \
+            ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
+            '(%s)</b></a>'%(self.classname, properties, width, height, label)
 
     def submit(self, label="Submit New Entry"):
         ''' Generate a submit button (and action hidden element)
@@ -223,56 +417,51 @@ class HTMLClass:
         ''' Render this class with the given template.
         '''
         # create a new request and override the specified args
-        req = HTMLRequest(self.client)
+        req = HTMLRequest(self._client)
         req.classname = self.classname
-        req.__dict__.update(kwargs)
+        req.update(kwargs)
 
         # new template, using the specified classname and request
-        pt = RoundupPageTemplate(self.client, self.classname, req)
+        pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
 
-        # use the specified template
-        name = self.classname + '.' + name
-        pt.write(open('/tmp/test/html/%s'%name).read())
-        pt.id = name
-
-        # XXX handle PT rendering errors here nicely
-        try:
-            return pt.render()
-        except PageTemplate.PTRuntimeError, message:
-            return '<strong>%s</strong><ol>%s</ol>'%(message,
-                cgi.escape('<li>'.join(pt._v_errors)))
+        # use our fabricated request
+        return pt.render(self._client, self.classname, req)
 
-class HTMLItem:
+class HTMLItem(HTMLPermissions):
     ''' Accesses through an *item*
     '''
-    def __init__(self, db, classname, nodeid):
-        self.db = db
-        self.classname = classname
-        self.nodeid = nodeid
-        self.klass = self.db.getclass(classname)
-        self.props = self.klass.getprops()
+    def __init__(self, client, classname, nodeid):
+        self._client = client
+        self._db = client.db
+        self._classname = classname
+        self._nodeid = nodeid
+        self._klass = self._db.getclass(classname)
+        self._props = self._klass.getprops()
 
     def __repr__(self):
-        return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
+        return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
+            self._nodeid)
 
     def __getitem__(self, item):
-        ''' return an HTMLItem instance'''
+        ''' return an HTMLProperty instance
+        '''
+       #print 'HTMLItem.getitem', (self, item)
         if item == 'id':
-            return self.nodeid
-        if not self.props.has_key(item):
-            raise KeyError, item
-        prop = self.props[item]
+            return self._nodeid
+
+        # get the property
+        prop = self._props[item]
 
         # get the value, handling missing values
-        value = self.klass.get(self.nodeid, item, None)
+        value = self._klass.get(self._nodeid, item, None)
         if value is None:
-            if isinstance(self.props[item], hyperdb.Multilink):
+            if isinstance(self._props[item], hyperdb.Multilink):
                 value = []
 
         # look up the correct HTMLProperty class
         for klass, htmlklass in propclasses:
             if isinstance(prop, klass):
-                return htmlklass(self.db, self.nodeid, prop, item, value)
+                return htmlklass(self._client, self._nodeid, prop, item, value)
 
         raise KeyErorr, item
 
@@ -289,17 +478,24 @@ class HTMLItem:
         return '  <input type="hidden" name=":action" value="edit">\n'\
         '  <input type="submit" name="submit" value="%s">'%label
 
-    # XXX this probably should just return the history items, not the HTML
+    def journal(self, direction='descending'):
+        ''' Return a list of HTMLJournalEntry instances.
+        '''
+        # XXX do this
+        return []
+
     def history(self, direction='descending'):
-        l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
-            '<tr class="list-header">',
-            _('<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>'),
+        l = ['<table class="history">'
+             '<tr><th colspan="4" class="header">',
+             _('History'),
+             '</th></tr><tr>',
+             _('<th>Date</th>'),
+             _('<th>User</th>'),
+             _('<th>Action</th>'),
+             _('<th>Args</th>'),
             '</tr>']
         comments = {}
-        history = self.klass.history(self.nodeid)
+        history = self._klass.history(self._nodeid)
         history.sort()
         if direction == 'descending':
             history.reverse()
@@ -328,7 +524,7 @@ class HTMLItem:
                     # try to get the relevant property and treat it
                     # specially
                     try:
-                        prop = self.props[k]
+                        prop = self._props[k]
                     except KeyError:
                         prop = None
                     if prop is not None:
@@ -337,14 +533,14 @@ class HTMLItem:
                             # figure what the link class is
                             classname = prop.classname
                             try:
-                                linkcl = self.db.getclass(classname)
+                                linkcl = self._db.getclass(classname)
                             except KeyError:
                                 labelprop = None
                                 comments[classname] = _('''The linked class
                                     %(classname)s no longer exists''')%locals()
                             labelprop = linkcl.labelprop(1)
                             hrefable = os.path.exists(
-                                os.path.join(self.db.config.TEMPLATES,
+                                os.path.join(self._db.config.TEMPLATES,
                                 classname+'.item'))
 
                         if isinstance(prop, hyperdb.Multilink) and \
@@ -427,9 +623,8 @@ class HTMLItem:
                     handled by the history display!</em></strong>''')
                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
             date_s = date_s.replace(' ', '&nbsp;')
-            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))
+            l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%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():
@@ -437,20 +632,30 @@ class HTMLItem:
         l.append('</table>')
         return '\n'.join(l)
 
-    def remove(self):
-        # XXX do what?
-        return ''
+    def renderQueryForm(self):
+        ''' Render this item, which is a query, as a search form.
+        '''
+        # create a new request and override the specified args
+        req = HTMLRequest(self._client)
+        req.classname = self._klass.get(self._nodeid, 'klass')
+        req.updateFromURL(self._klass.get(self._nodeid, 'url'))
+
+        # new template, using the specified classname and request
+        pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
+
+        # use our fabricated request
+        return pt.render(self._client, req.classname, req)
 
 class HTMLUser(HTMLItem):
     ''' Accesses through the *user* (a special case of item)
     '''
-    def __init__(self, client):
-        HTMLItem.__init__(self, client.db, 'user', client.userid)
-        self.default_classname = client.classname
-        self.userid = client.userid
+    def __init__(self, client, classname, nodeid):
+        HTMLItem.__init__(self, client, 'user', nodeid)
+        self._default_classname = client.classname
 
         # used for security checks
-        self.security = client.db.security
+        self._security = client.db.security
+
     _marker = []
     def hasPermission(self, role, classname=_marker):
         ''' Determine if the user has the Role.
@@ -459,149 +664,219 @@ class HTMLUser(HTMLItem):
             be overidden for this test by suppling an alternate classname.
         '''
         if classname is self._marker:
-            classname = self.default_classname
-        return self.security.hasPermission(role, self.userid, classname)
+            classname = self._default_classname
+        return self._security.hasPermission(role, self._nodeid, classname)
+
+    def is_edit_ok(self):
+        ''' Is the user allowed to Edit the current class?
+            Also check whether this is the current user's info.
+        '''
+        return self._db.security.hasPermission('Edit', self._client.userid,
+            self._classname) or self._nodeid == self._client.userid
+
+    def is_view_ok(self):
+        ''' Is the user allowed to View the current class?
+            Also check whether this is the current user's info.
+        '''
+        return self._db.security.hasPermission('Edit', self._client.userid,
+            self._classname) or self._nodeid == self._client.userid
 
 class HTMLProperty:
     ''' String, Number, Date, Interval HTMLProperty
 
+        Has useful attributes:
+
+         _name  the name of the property
+         _value the value of the property if any
+
         A wrapper object which may be stringified for the plain() behaviour.
     '''
-    def __init__(self, db, nodeid, prop, name, value):
-        self.db = db
-        self.nodeid = nodeid
-        self.prop = prop
-        self.name = name
-        self.value = value
+    def __init__(self, client, nodeid, prop, name, value):
+        self._client = client
+        self._db = client.db
+        self._nodeid = nodeid
+        self._prop = prop
+        self._name = name
+        self._value = value
     def __repr__(self):
-        return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
+        return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
     def __str__(self):
         return self.plain()
     def __cmp__(self, other):
         if isinstance(other, HTMLProperty):
-            return cmp(self.value, other.value)
-        return cmp(self.value, other)
+            return cmp(self._value, other._value)
+        return cmp(self._value, other)
 
 class StringHTMLProperty(HTMLProperty):
     def plain(self, escape=0):
-        if self.value is None:
+        ''' Render a "plain" representation of the property
+        '''
+        if self._value is None:
             return ''
         if escape:
-            return cgi.escape(str(self.value))
-        return str(self.value)
+            return cgi.escape(str(self._value))
+        return str(self._value)
 
     def stext(self, escape=0):
+        ''' Render the value of the property as StructuredText.
+
+            This requires the StructureText module to be installed separately.
+        '''
         s = self.plain(escape=escape)
         if not StructuredText:
             return s
         return StructuredText(s,level=1,header=0)
 
     def field(self, size = 30):
-        if self.value is None:
+        ''' Render a form edit field for the property
+        '''
+        if self._value is None:
             value = ''
         else:
-            value = cgi.escape(str(self.value))
+            value = cgi.escape(str(self._value))
             value = '&quot;'.join(value.split('"'))
-        return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+        return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
 
     def multiline(self, escape=0, rows=5, cols=40):
-        if self.value is None:
+        ''' Render a multiline form edit field for the property
+        '''
+        if self._value is None:
             value = ''
         else:
-            value = cgi.escape(str(self.value))
+            value = cgi.escape(str(self._value))
             value = '&quot;'.join(value.split('"'))
         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
-            self.name, rows, cols, value)
+            self._name, rows, cols, value)
 
     def email(self, escape=1):
-        ''' fudge email '''
-        if self.value is None: value = ''
-        else: value = str(self.value)
-        value = value.replace('@', ' at ')
-        value = value.replace('.', ' ')
+        ''' Render the value of the property as an obscured email address
+        '''
+        if self._value is None: value = ''
+        else: value = str(self._value)
+        if value.find('@') != -1:
+            name, domain = value.split('@')
+            domain = ' '.join(domain.split('.')[:-1])
+            name = name.replace('.', ' ')
+            value = '%s at %s ...'%(name, domain)
+        else:
+            value = value.replace('.', ' ')
         if escape:
             value = cgi.escape(value)
         return value
 
 class PasswordHTMLProperty(HTMLProperty):
     def plain(self):
-        if self.value is None:
+        ''' Render a "plain" representation of the property
+        '''
+        if self._value is None:
             return ''
         return _('*encrypted*')
 
     def field(self, size = 30):
-        return '<input type="password" name="%s" size="%s">'%(self.name, size)
+        ''' Render a form edit field for the property.
+        '''
+        return '<input type="password" name="%s" size="%s">'%(self._name, size)
+
+    def confirm(self, size = 30):
+        ''' Render a second form edit field for the property, used for 
+            confirmation that the user typed the password correctly. Generates
+            a field with name "name:confirm".
+        '''
+        return '<input type="password" name="%s:confirm" size="%s">'%(
+            self._name, size)
 
 class NumberHTMLProperty(HTMLProperty):
     def plain(self):
-        return str(self.value)
+        ''' Render a "plain" representation of the property
+        '''
+        return str(self._value)
 
     def field(self, size = 30):
-        if self.value is None:
+        ''' Render a form edit field for the property
+        '''
+        if self._value is None:
             value = ''
         else:
-            value = cgi.escape(str(self.value))
+            value = cgi.escape(str(self._value))
             value = '&quot;'.join(value.split('"'))
-        return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+        return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
 
 class BooleanHTMLProperty(HTMLProperty):
     def plain(self):
+        ''' Render a "plain" representation of the property
+        '''
         if self.value is None:
             return ''
-        return self.value and "Yes" or "No"
+        return self._value and "Yes" or "No"
 
     def field(self):
-        checked = self.value and "checked" or ""
-        s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
+        ''' Render a form edit field for the property
+        '''
+        checked = self._value and "checked" or ""
+        s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
             checked)
         if checked:
             checked = ""
         else:
             checked = "checked"
-        s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
+        s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
             checked)
         return s
 
 class DateHTMLProperty(HTMLProperty):
     def plain(self):
-        if self.value is None:
+        ''' Render a "plain" representation of the property
+        '''
+        if self._value is None:
             return ''
-        return str(self.value)
+        return str(self._value)
 
     def field(self, size = 30):
-        if self.value is None:
+        ''' Render a form edit field for the property
+        '''
+        if self._value is None:
             value = ''
         else:
-            value = cgi.escape(str(self.value))
+            value = cgi.escape(str(self._value))
             value = '&quot;'.join(value.split('"'))
-        return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+        return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
 
     def reldate(self, pretty=1):
-        if not self.value:
+        ''' Render the interval between the date and now.
+
+            If the "pretty" flag is true, then make the display pretty.
+        '''
+        if not self._value:
             return ''
 
         # figure the interval
-        interval = date.Date('.') - self.value
+        interval = date.Date('.') - self._value
         if pretty:
             return interval.pretty()
         return str(interval)
 
 class IntervalHTMLProperty(HTMLProperty):
     def plain(self):
-        if self.value is None:
+        ''' Render a "plain" representation of the property
+        '''
+        if self._value is None:
             return ''
-        return str(self.value)
+        return str(self._value)
 
     def pretty(self):
-        return self.value.pretty()
+        ''' Render the interval in a pretty format (eg. "yesterday")
+        '''
+        return self._value.pretty()
 
     def field(self, size = 30):
-        if self.value is None:
+        ''' Render a form edit field for the property
+        '''
+        if self._value is None:
             value = ''
         else:
-            value = cgi.escape(str(self.value))
+            value = cgi.escape(str(self._value))
             value = '&quot;'.join(value.split('"'))
-        return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+        return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
 
 class LinkHTMLProperty(HTMLProperty):
     ''' Link HTMLProperty
@@ -615,37 +890,41 @@ class LinkHTMLProperty(HTMLProperty):
     '''
     def __getattr__(self, attr):
         ''' return a new HTMLItem '''
-        #print 'getattr', (self, attr, self.value)
-        if not self.value:
+       #print 'Link.getattr', (self, attr, self._value)
+        if not self._value:
             raise AttributeError, "Can't access missing value"
-        i = HTMLItem(self.db, self.prop.classname, self.value)
+        if self._prop.classname == 'user':
+            klass = HTMLUser
+        else:
+            klass = HTMLItem
+        i = klass(self._client, self._prop.classname, self._value)
         return getattr(i, attr)
 
     def plain(self, escape=0):
-        if self.value is None:
-            return _('[unselected]')
-        linkcl = self.db.classes[self.prop.classname]
+        ''' Render a "plain" representation of the property
+        '''
+        if self._value is None:
+            return ''
+        linkcl = self._db.classes[self._prop.classname]
         k = linkcl.labelprop(1)
-        value = str(linkcl.get(self.value, k))
+        value = str(linkcl.get(self._value, k))
         if escape:
             value = cgi.escape(value)
         return value
 
-    # XXX most of the stuff from here down is of dubious utility - it's easy
-    # enough to do in the template by hand (and in some cases, it's shorter
-    # and clearer...
-
-    def field(self):
-        linkcl = self.db.getclass(self.prop.classname)
+    def field(self, showid=0, size=None):
+        ''' Render a form edit field for the property
+        '''
+        linkcl = self._db.getclass(self._prop.classname)
         if linkcl.getprops().has_key('order'):  
             sort_on = 'order'  
         else:  
             sort_on = linkcl.labelprop()  
-        options = linkcl.filter(None, {}, [sort_on], []) 
+        options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
         # TODO: make this a field display, not a menu one!
-        l = ['<select name="%s">'%property]
+        l = ['<select name="%s">'%self._name]
         k = linkcl.labelprop(1)
-        if value is None:
+        if self._value is None:
             s = 'selected '
         else:
             s = ''
@@ -653,10 +932,10 @@ class LinkHTMLProperty(HTMLProperty):
         for optionid in options:
             option = linkcl.get(optionid, k)
             s = ''
-            if optionid == value:
+            if optionid == self._value:
                 s = 'selected '
             if showid:
-                lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+                lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
             if size is not None and len(lab) > size:
@@ -666,33 +945,17 @@ class LinkHTMLProperty(HTMLProperty):
         l.append('</select>')
         return '\n'.join(l)
 
-    def download(self, showid=0):
-        linkname = self.prop.classname
-        linkcl = self.db.getclass(linkname)
-        k = linkcl.labelprop(1)
-        linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
-        if showid:
-            label = value
-            title = ' title="%s"'%linkvalue
-            # note ... this should be urllib.quote(linkcl.get(value, k))
-        else:
-            label = linkvalue
-            title = ''
-        return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
-            linkvalue, title, label)
-
     def menu(self, size=None, height=None, showid=0, additional=[],
             **conditions):
-        value = self.value
+        ''' Render a form select list for this property
+        '''
+        value = self._value
 
         # sort function
-        sortfunc = make_sort_function(self.db, self.prop.classname)
+        sortfunc = make_sort_function(self._db, self._prop.classname)
 
-        # force the value to be a single choice
-        if isinstance(value, type('')):
-            value = value[0]
-        linkcl = self.db.getclass(self.prop.classname)
-        l = ['<select name="%s">'%self.name]
+        linkcl = self._db.getclass(self._prop.classname)
+        l = ['<select name="%s">'%self._name]
         k = linkcl.labelprop(1)
         s = ''
         if value is None:
@@ -709,7 +972,7 @@ class LinkHTMLProperty(HTMLProperty):
             if value in [optionid, option]:
                 s = 'selected '
             if showid:
-                lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+                lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
             if size is not None and len(lab) > size:
@@ -723,7 +986,6 @@ class LinkHTMLProperty(HTMLProperty):
             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
         l.append('</select>')
         return '\n'.join(l)
-
 #    def checklist(self, ...)
 
 class MultilinkHTMLProperty(HTMLProperty):
@@ -734,67 +996,86 @@ class MultilinkHTMLProperty(HTMLProperty):
     '''
     def __len__(self):
         ''' length of the multilink '''
-        return len(self.value)
+        return len(self._value)
 
     def __getattr__(self, attr):
         ''' no extended attribute accesses make sense here '''
         raise AttributeError, attr
 
     def __getitem__(self, num):
-        ''' iterate and return a new HTMLItem '''
-        #print 'getitem', (self, num)
-        value = self.value[num]
-        return HTMLItem(self.db, self.prop.classname, value)
+        ''' iterate and return a new HTMLItem
+        '''
+       #print 'Multi.getitem', (self, num)
+        value = self._value[num]
+        if self._prop.classname == 'user':
+            klass = HTMLUser
+        else:
+            klass = HTMLItem
+        return klass(self._client, self._prop.classname, value)
+
+    def __contains__(self, value):
+        ''' Support the "in" operator
+        '''
+        return value in self._value
 
     def reverse(self):
-        ''' return the list in reverse order '''
-        l = self.value[:]
+        ''' return the list in reverse order
+        '''
+        l = self._value[:]
         l.reverse()
-        return [HTMLItem(self.db, self.prop.classname, value) for value in l]
+        if self._prop.classname == 'user':
+            klass = HTMLUser
+        else:
+            klass = HTMLItem
+        return [klass(self._client, self._prop.classname, value) for value in l]
 
     def plain(self, escape=0):
-        linkcl = self.db.classes[self.prop.classname]
+        ''' Render a "plain" representation of the property
+        '''
+        linkcl = self._db.classes[self._prop.classname]
         k = linkcl.labelprop(1)
         labels = []
-        for v in self.value:
+        for v in self._value:
             labels.append(linkcl.get(v, k))
         value = ', '.join(labels)
         if escape:
             value = cgi.escape(value)
         return value
 
-    # XXX most of the stuff from here down is of dubious utility - it's easy
-    # enough to do in the template by hand (and in some cases, it's shorter
-    # and clearer...
-
     def field(self, size=30, showid=0):
-        sortfunc = make_sort_function(self.db, self.prop.classname)
-        linkcl = self.db.getclass(self.prop.classname)
-        value = self.value[:]
+        ''' Render a form edit field for the property
+        '''
+        sortfunc = make_sort_function(self._db, self._prop.classname)
+        linkcl = self._db.getclass(self._prop.classname)
+        value = self._value[:]
         if value:
             value.sort(sortfunc)
         # map the id to the label property
+        if not linkcl.getkey():
+            showid=1
         if not showid:
             k = linkcl.labelprop(1)
             value = [linkcl.get(v, k) for v in value]
         value = cgi.escape(','.join(value))
-        return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
+        return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
 
     def menu(self, size=None, height=None, showid=0, additional=[],
             **conditions):
-        value = self.value
+        ''' Render a form select list for this property
+        '''
+        value = self._value
 
         # sort function
-        sortfunc = make_sort_function(self.db, self.prop.classname)
+        sortfunc = make_sort_function(self._db, self._prop.classname)
 
-        linkcl = self.db.getclass(self.prop.classname)
+        linkcl = self._db.getclass(self._prop.classname)
         if linkcl.getprops().has_key('order'):  
             sort_on = ('+', 'order')
         else:  
             sort_on = ('+', linkcl.labelprop())
         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
         height = height or min(len(options), 7)
-        l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
+        l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
         k = linkcl.labelprop(1)
         for optionid in options:
             option = linkcl.get(optionid, k)
@@ -802,7 +1083,7 @@ class MultilinkHTMLProperty(HTMLProperty):
             if optionid in value or option in value:
                 s = 'selected '
             if showid:
-                lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+                lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
             if size is not None and len(lab) > size:
@@ -849,13 +1130,41 @@ def handleListCGIValue(value):
     if isinstance(value, type([])):
         return [value.value for value in value]
     else:
-        return value.value.split(',')
+        value = value.value.strip()
+        if not value:
+            return []
+        return value.split(',')
+
+class ShowDict:
+    ''' A convenience access to the :columns index parameters
+    '''
+    def __init__(self, columns):
+        self.columns = {}
+        for col in columns:
+            self.columns[col] = 1
+    def __getitem__(self, name):
+        return self.columns.has_key(name)
 
-# XXX This is starting to look a lot (in data terms) like the client object
-# itself!
 class HTMLRequest:
     ''' The *request*, holding the CGI form and environment.
 
+        "form" the CGI form as a cgi.FieldStorage
+        "env" the CGI environment variables
+        "base" the base URL for this instance
+        "user" a HTMLUser instance for this user
+        "classname" the current classname (possibly None)
+        "template" the current template (suffix, also possibly None)
+
+        Index args:
+        "columns" dictionary of the columns to display in an index page
+        "show" a convenience access to columns - request/show/colname will
+               be true if the columns should be displayed, false otherwise
+        "sort" index sort column (direction, column name)
+        "group" index grouping property (direction, column name)
+        "filter" properties to filter the index on
+        "filterspec" values to filter the index on
+        "search_text" text to perform a full-text search on for an index
+
     '''
     def __init__(self, client):
         self.client = client
@@ -864,17 +1173,22 @@ class HTMLRequest:
         self.form = client.form
         self.env = client.env
         self.base = client.base
-        self.user = HTMLUser(client)
+        self.user = HTMLUser(client, 'user', client.userid)
 
         # store the current class name and action
         self.classname = client.classname
-        self.template_type = client.template_type
+        self.template = client.template
+
+        self._post_init()
 
+    def _post_init(self):
+        ''' Set attributes based on self.form
+        '''
         # extract the index display information from the form
-        self.columns = {}
+        self.columns = []
         if self.form.has_key(':columns'):
-            for entry in handleListCGIValue(self.form[':columns']):
-                self.columns[entry] = 1
+            self.columns = handleListCGIValue(self.form[':columns'])
+        self.show = ShowDict(self.columns)
 
         # sorting
         self.sort = (None, None)
@@ -903,21 +1217,75 @@ class HTMLRequest:
         if self.form.has_key(':filter'):
             self.filter = handleListCGIValue(self.form[':filter'])
         self.filterspec = {}
-        props = self.client.db.getclass(self.classname).getprops()
-        for name in self.filter:
-            if self.form.has_key(name):
-                prop = props[name]
-                if (isinstance(prop, hyperdb.Link) or
-                        isinstance(prop, hyperdb.Multilink)):
-                    self.filterspec[name] = handleListCGIValue(self.form[name])
-                else:
-                    self.filterspec[name] = self.form[name].value
+        if self.classname is not None:
+            props = self.client.db.getclass(self.classname).getprops()
+            for name in self.filter:
+                if self.form.has_key(name):
+                    prop = props[name]
+                    fv = self.form[name]
+                    if (isinstance(prop, hyperdb.Link) or
+                            isinstance(prop, hyperdb.Multilink)):
+                        self.filterspec[name] = handleListCGIValue(fv)
+                    else:
+                        self.filterspec[name] = fv.value
 
         # full-text search argument
         self.search_text = None
         if self.form.has_key(':search_text'):
             self.search_text = self.form[':search_text'].value
 
+        # pagination - size and start index
+        # figure batch args
+        if self.form.has_key(':pagesize'):
+            self.pagesize = int(self.form[':pagesize'].value)
+        else:
+            self.pagesize = 50
+        if self.form.has_key(':startwith'):
+            self.startwith = int(self.form[':startwith'].value)
+        else:
+            self.startwith = 0
+
+    def updateFromURL(self, url):
+        ''' Parse the URL for query args, and update my attributes using the
+            values.
+        ''' 
+        self.form = {}
+        for name, value in cgi.parse_qsl(url):
+            if self.form.has_key(name):
+                if isinstance(self.form[name], type([])):
+                    self.form[name].append(cgi.MiniFieldStorage(name, value))
+                else:
+                    self.form[name] = [self.form[name],
+                        cgi.MiniFieldStorage(name, value)]
+            else:
+                self.form[name] = cgi.MiniFieldStorage(name, value)
+        self._post_init()
+
+    def update(self, kwargs):
+        ''' Update my attributes using the keyword args
+        '''
+        self.__dict__.update(kwargs)
+        if kwargs.has_key('columns'):
+            self.show = ShowDict(self.columns)
+
+    def description(self):
+        ''' Return a description of the request - handle for the page title.
+        '''
+        s = [self.client.db.config.TRACKER_NAME]
+        if self.classname:
+            if self.client.nodeid:
+                s.append('- %s%s'%(self.classname, self.client.nodeid))
+            else:
+                if self.template == 'item':
+                    s.append('- new %s'%self.classname)
+                elif self.template == 'index':
+                    s.append('- %s index'%self.classname)
+                else:
+                    s.append('- %s %s'%(self.classname, self.template))
+        else:
+            s.append('- home')
+        return ' '.join(s)
+
     def __str__(self):
         d = {}
         d.update(self.__dict__)
@@ -931,14 +1299,17 @@ class HTMLRequest:
         d['env'] = e
         return '''
 form: %(form)s
+url: %(url)r
 base: %(base)r
 classname: %(classname)r
-template_type: %(template_type)r
+template: %(template)r
 columns: %(columns)r
 sort: %(sort)r
 group: %(group)r
 filter: %(filter)r
-filterspec: %(filterspec)r
+search_text: %(search_text)r
+pagesize: %(pagesize)r
+startwith: %(startwith)r
 env: %(env)s
 '''%d
 
@@ -948,31 +1319,60 @@ env: %(env)s
         l = []
         s = '<input type="hidden" name="%s" value="%s">'
         if columns and self.columns:
-            l.append(s%(':columns', ','.join(self.columns.keys())))
-        if sort and self.sort is not None:
-            l.append(s%(':sort', self.sort))
-        if group and self.group is not None:
-            l.append(s%(':group', self.group))
+            l.append(s%(':columns', ','.join(self.columns)))
+        if sort and self.sort[1] is not None:
+            if self.sort[0] == '-':
+                val = '-'+self.sort[1]
+            else:
+                val = self.sort[1]
+            l.append(s%(':sort', val))
+        if group and self.group[1] is not None:
+            if self.group[0] == '-':
+                val = '-'+self.group[1]
+            else:
+                val = self.group[1]
+            l.append(s%(':group', val))
         if filter and self.filter:
             l.append(s%(':filter', ','.join(self.filter)))
         if filterspec:
             for k,v in self.filterspec.items():
                 l.append(s%(k, ','.join(v)))
+        if self.search_text:
+            l.append(s%(':search_text', self.search_text))
+        l.append(s%(':pagesize', self.pagesize))
+        l.append(s%(':startwith', self.startwith))
         return '\n'.join(l)
 
-    def indexargs_href(self, url, args):
+    def indexargs_url(self, url, args):
+        ''' embed the current index args in a URL '''
         l = ['%s=%s'%(k,v) for k,v in args.items()]
-        if self.columns:
-            l.append(':columns=%s'%(','.join(self.columns.keys())))
-        if self.sort is not None:
-            l.append(':sort=%s'%self.sort)
-        if self.group is not None:
-            l.append(':group=%s'%self.group)
-        if self.filter:
+        if self.columns and not args.has_key(':columns'):
+            l.append(':columns=%s'%(','.join(self.columns)))
+        if self.sort[1] is not None and not args.has_key(':sort'):
+            if self.sort[0] == '-':
+                val = '-'+self.sort[1]
+            else:
+                val = self.sort[1]
+            l.append(':sort=%s'%val)
+        if self.group[1] is not None and not args.has_key(':group'):
+            if self.group[0] == '-':
+                val = '-'+self.group[1]
+            else:
+                val = self.group[1]
+            l.append(':group=%s'%val)
+        if self.filter and not args.has_key(':columns'):
             l.append(':filter=%s'%(','.join(self.filter)))
         for k,v in self.filterspec.items():
-            l.append('%s=%s'%(k, ','.join(v)))
+            if not args.has_key(k):
+                l.append('%s=%s'%(k, ','.join(v)))
+        if self.search_text and not args.has_key(':search_text'):
+            l.append(':search_text=%s'%self.search_text)
+        if not args.has_key(':pagesize'):
+            l.append(':pagesize=%s'%self.pagesize)
+        if not args.has_key(':startwith'):
+            l.append(':startwith=%s'%self.startwith)
         return '%s?%s'%(url, '&'.join(l))
+    indexargs_href = indexargs_url
 
     def base_javascript(self):
         return '''
@@ -1009,26 +1409,49 @@ function help_window(helpurl, width, height) {
             matches = None
         l = klass.filter(matches, filterspec, sort, group)
 
-        # figure batch args
-        if self.form.has_key(':pagesize'):
-            size = int(self.form[':pagesize'].value)
-        else:
-            size = 50
-        if self.form.has_key(':startwith'):
-            start = int(self.form[':startwith'].value)
+        # map the item ids to instances
+        if self.classname == 'user':
+            klass = HTMLUser
         else:
-            start = 0
+            klass = HTMLItem
+        l = [klass(self.client, self.classname, item) for item in l]
 
         # return the batch object
-        return Batch(self.client, self.classname, l, size, start)
+        return Batch(self.client, l, self.pagesize, self.startwith)
 
+# extend the standard ZTUtils Batch object to remove dependency on
+# Acquisition and add a couple of useful methods
 class Batch(ZTUtils.Batch):
-    def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
+    ''' Use me to turn a list of items, or item ids of a given class, into a
+        series of batches.
+
+        ========= ========================================================
+        Parameter  Usage
+        ========= ========================================================
+        sequence  a list of HTMLItems
+        size      how big to make the sequence.
+        start     where to start (0-indexed) in the sequence.
+        end       where to end (0-indexed) in the sequence.
+        orphan    if the next batch would contain less items than this
+                  value, then it is combined with this batch
+        overlap   the number of items shared between adjacent batches
+        ========= ========================================================
+
+        Attributes: Note that the "start" attribute, unlike the
+        argument, is a 1-based index (I know, lame).  "first" is the
+        0-based index.  "length" is the actual number of elements in
+        the batch.
+
+        "sequence_length" is the length of the original, unbatched, sequence.
+    '''
+    def __init__(self, client, sequence, size, start, end=0, orphan=0,
+            overlap=0):
         self.client = client
-        self.classname = classname
         self.last_index = self.last_item = None
         self.current_item = None
-        ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
+        self.sequence_length = len(sequence)
+        ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
+            overlap)
 
     # overwrite so we can late-instantiate the HTMLItem instance
     def __getitem__(self, index):
@@ -1036,7 +1459,8 @@ class Batch(ZTUtils.Batch):
             if index + self.end < self.first: raise IndexError, index
             return self._sequence[index + self.end]
         
-        if index >= self.length: raise IndexError, index
+        if index >= self.length:
+            raise IndexError, index
 
         # move the last_item along - but only if the fetched index changes
         # (for some reason, index 0 is fetched twice)
@@ -1044,9 +1468,7 @@ class Batch(ZTUtils.Batch):
             self.last_item = self.current_item
             self.last_index = index
 
-        # wrap the return in an HTMLItem
-        self.current_item = HTMLItem(self.client.db, self.classname,
-            self._sequence[index+self.first])
+        self.current_item = self._sequence[index + self.first]
         return self.current_item
 
     def propchanged(self, property):
@@ -1062,7 +1484,7 @@ class Batch(ZTUtils.Batch):
     def previous(self):
         if self.start == 1:
             return None
-        return Batch(self.client, self.classname, self._sequence, self._size,
+        return Batch(self.client, self._sequence, self._size,
             self.first - self._size + self.overlap, 0, self.orphan,
             self.overlap)
 
@@ -1071,10 +1493,15 @@ class Batch(ZTUtils.Batch):
             self._sequence[self.end]
         except IndexError:
             return None
-        return Batch(self.client, self.classname, self._sequence, self._size,
+        return Batch(self.client, self._sequence, self._size,
             self.end - self.overlap, 0, self.orphan, self.overlap)
 
-    def length(self):
-        self.sequence_length = l = len(self._sequence)
-        return l
+class TemplatingUtils:
+    ''' Utilities for templating
+    '''
+    def __init__(self, client):
+        self.client = client
+    def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
+        return Batch(self.client, sequence, size, start, end, orphan,
+            overlap)