Code

missed some of the creator prop spec fixes .. metakit may be busted by this
[roundup.git] / roundup / cgi / templating.py
index 9292c49f194433f66a3d12bd28ef7dfbe4d35205..1de56e1ce1d42b1503959af6015915b1961f6203 100644 (file)
@@ -150,11 +150,15 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
              'request': request,
              'content': client.content,
              'db': HTMLDatabase(client),
-             'instance': client.instance
+             'instance': client.instance,
+             'utils': TemplatingUtils(client),
         }
         # add in the item if there is one
         if client.nodeid:
-            c['context'] = HTMLItem(client, classname, client.nodeid)
+            if classname == 'user':
+                c['context'] = HTMLUser(client, classname, client.nodeid)
+            else:
+                c['context'] = HTMLItem(client, classname, client.nodeid)
         else:
             c['context'] = HTMLClass(client, classname)
         return c
@@ -169,7 +173,7 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
 
         if self._v_errors:
             raise PageTemplate.PTRuntimeError, \
-                'Page Template %s has errors.' % self.id
+                'Page Template %s has errors.'%self.id
 
         # figure the context
         classname = classname or client.classname
@@ -192,26 +196,59 @@ class HTMLDatabase:
         # 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):
         try:
-            self._client.db.getclass(attr)
+            return self[attr]
         except KeyError:
             raise AttributeError, attr
-        return HTMLClass(self._client, attr)
+
     def classes(self):
         l = self._client.db.classes.keys()
         l.sort()
         return [HTMLClass(self._client, cn) for cn in l]
-        
-class HTMLClass:
+
+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
 
-        # we want classname to be exposed
-        self.classname = classname
+        # 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()
@@ -222,26 +259,38 @@ class HTMLClass:
     def __getitem__(self, item):
         ''' return an HTMLProperty instance
         '''
-        #print 'getitem', (self, item)
+       #print 'HTMLClass.getitem', (self, item)
 
         # we don't exist
         if item == 'id':
             return None
-        if item == 'creator':
-            # but we will be created by this user...
-            return HTMLUser(self._client, 'user', self._client.userid)
 
         # 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._client, '', prop, item, value)
+                if isinstance(prop, hyperdb.Multilink):
+                    value = []
+                else:
+                    value = None
+            return htmlklass(self._client, '', prop, item, value)
 
         # no good
         raise KeyError, item
@@ -254,7 +303,7 @@ 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():
@@ -268,11 +317,19 @@ class HTMLClass:
         return l
 
     def list(self):
+        ''' List all items in this class.
+        '''
         if self.classname == 'user':
             klass = HTMLUser
         else:
             klass = HTMLItem
-        l = [klass(self._client, self.classname, x) for x in self._klass.list()]
+
+        # get the list and sort it nicely
+        l = self._klass.list()
+        sortfunc = make_sort_function(self._db, self.classname)
+        l.sort(sortfunc)
+
+        l = [klass(self._client, self.classname, x) for x in l]
         return l
 
     def csv(self):
@@ -325,17 +382,24 @@ class HTMLClass:
              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.
         '''
+        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)
@@ -360,15 +424,10 @@ class HTMLClass:
         # new template, using the specified classname and request
         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
 
-        # XXX handle PT rendering errors here nicely
-        try:
-            # use our fabricated request
-            return pt.render(self._client, self.classname, req)
-        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, client, classname, nodeid):
@@ -386,7 +445,7 @@ class HTMLItem:
     def __getitem__(self, item):
         ''' return an HTMLProperty instance
         '''
-        #print 'getitem', (self, item)
+        #print 'HTMLItem.getitem', (self, item)
         if item == 'id':
             return self._nodeid
 
@@ -419,7 +478,12 @@ 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 class="history">'
              '<tr><th colspan="4" class="header">',
@@ -568,6 +632,20 @@ class HTMLItem:
         l.append('</table>')
         return '\n'.join(l)
 
+    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)
     '''
@@ -577,6 +655,7 @@ class HTMLUser(HTMLItem):
 
         # used for security checks
         self._security = client.db.security
+
     _marker = []
     def hasPermission(self, role, classname=_marker):
         ''' Determine if the user has the Role.
@@ -588,9 +667,28 @@ class HTMLUser(HTMLItem):
             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, client, nodeid, prop, name, value):
@@ -611,6 +709,8 @@ class HTMLProperty:
 
 class StringHTMLProperty(HTMLProperty):
     def plain(self, escape=0):
+        ''' Render a "plain" representation of the property
+        '''
         if self._value is None:
             return ''
         if escape:
@@ -618,12 +718,18 @@ class StringHTMLProperty(HTMLProperty):
         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):
+        ''' Render a form edit field for the property
+        '''
         if self._value is None:
             value = ''
         else:
@@ -632,6 +738,8 @@ class StringHTMLProperty(HTMLProperty):
         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
 
     def multiline(self, escape=0, rows=5, cols=40):
+        ''' Render a multiline form edit field for the property
+        '''
         if self._value is None:
             value = ''
         else:
@@ -641,29 +749,51 @@ class StringHTMLProperty(HTMLProperty):
             self._name, rows, cols, value)
 
     def email(self, escape=1):
-        ''' fudge email '''
-        if self.value is None: value = ''
+        ''' Render the value of the property as an obscured email address
+        '''
+        if self._value is None: value = ''
         else: value = str(self._value)
-        value = value.replace('@', ' at ')
-        value = value.replace('.', ' ')
+        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):
+        ''' Render a "plain" representation of the property
+        '''
         if self._value is None:
             return ''
         return _('*encrypted*')
 
     def field(self, size = 30):
+        ''' 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):
+        ''' Render a "plain" representation of the property
+        '''
         return str(self._value)
 
     def field(self, size = 30):
+        ''' Render a form edit field for the property
+        '''
         if self._value is None:
             value = ''
         else:
@@ -673,11 +803,15 @@ class NumberHTMLProperty(HTMLProperty):
 
 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"
 
     def field(self):
+        ''' 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)
@@ -691,11 +825,15 @@ class BooleanHTMLProperty(HTMLProperty):
 
 class DateHTMLProperty(HTMLProperty):
     def plain(self):
+        ''' Render a "plain" representation of the property
+        '''
         if self._value is None:
             return ''
         return str(self._value)
 
     def field(self, size = 30):
+        ''' Render a form edit field for the property
+        '''
         if self._value is None:
             value = ''
         else:
@@ -704,6 +842,10 @@ class DateHTMLProperty(HTMLProperty):
         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
 
     def reldate(self, pretty=1):
+        ''' Render the interval between the date and now.
+
+            If the "pretty" flag is true, then make the display pretty.
+        '''
         if not self._value:
             return ''
 
@@ -715,14 +857,20 @@ class DateHTMLProperty(HTMLProperty):
 
 class IntervalHTMLProperty(HTMLProperty):
     def plain(self):
+        ''' Render a "plain" representation of the property
+        '''
         if self._value is None:
             return ''
         return str(self._value)
 
     def pretty(self):
+        ''' Render the interval in a pretty format (eg. "yesterday")
+        '''
         return self._value.pretty()
 
     def field(self, size = 30):
+        ''' Render a form edit field for the property
+        '''
         if self._value is None:
             value = ''
         else:
@@ -742,19 +890,21 @@ class LinkHTMLProperty(HTMLProperty):
     '''
     def __getattr__(self, attr):
         ''' return a new HTMLItem '''
-        #print 'getattr', (self, attr, self._value)
+       #print 'Link.getattr', (self, attr, self._value)
         if not self._value:
             raise AttributeError, "Can't access missing value"
         if self._prop.classname == 'user':
-            klass = HTMLItem
-        else:
             klass = HTMLUser
+        else:
+            klass = HTMLItem
         i = klass(self._client, self._prop.classname, self._value)
         return getattr(i, attr)
 
     def plain(self, escape=0):
+        ''' Render a "plain" representation of the property
+        '''
         if self._value is None:
-            return _('[unselected]')
+            return ''
         linkcl = self._db.classes[self._prop.classname]
         k = linkcl.labelprop(1)
         value = str(linkcl.get(self._value, k))
@@ -762,62 +912,57 @@ class LinkHTMLProperty(HTMLProperty):
             value = cgi.escape(value)
         return value
 
-    def field(self):
+    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 = ''
         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
         for optionid in options:
-            option = linkcl.get(optionid, k)
+            # get the option value, and if it's None use an empty string
+            option = linkcl.get(optionid, k) or ''
+
+            # figure if this option is selected
             s = ''
-            if optionid == value:
+            if optionid == self._value:
                 s = 'selected '
+
+            # figure the label
             if showid:
                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
+
+            # truncate if it's too long
             if size is not None and len(lab) > size:
                 lab = lab[:size-3] + '...'
+
+            # and generate
             lab = cgi.escape(lab)
             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
         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):
+        ''' Render a form select list for this property
+        '''
         value = self._value
 
         # sort function
         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]
         k = linkcl.labelprop(1)
@@ -831,14 +976,21 @@ class LinkHTMLProperty(HTMLProperty):
             sort_on = ('+', linkcl.labelprop())
         options = linkcl.filter(None, conditions, sort_on, (None, None))
         for optionid in options:
-            option = linkcl.get(optionid, k)
+            # get the option value, and if it's None use an empty string
+            option = linkcl.get(optionid, k) or ''
+
+            # figure if this option is selected
             s = ''
             if value in [optionid, option]:
                 s = 'selected '
+
+            # figure the label
             if showid:
                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
+
+            # truncate if it's too long
             if size is not None and len(lab) > size:
                 lab = lab[:size-3] + '...'
             if additional:
@@ -846,11 +998,12 @@ class LinkHTMLProperty(HTMLProperty):
                 for propname in additional:
                     m.append(linkcl.get(optionid, propname))
                 lab = lab + ' (%s)'%', '.join(map(str, m))
+
+            # and generate
             lab = cgi.escape(lab)
             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
         l.append('</select>')
         return '\n'.join(l)
-
 #    def checklist(self, ...)
 
 class MultilinkHTMLProperty(HTMLProperty):
@@ -870,7 +1023,7 @@ class MultilinkHTMLProperty(HTMLProperty):
     def __getitem__(self, num):
         ''' iterate and return a new HTMLItem
         '''
-        #print 'getitem', (self, num)
+       #print 'Multi.getitem', (self, num)
         value = self._value[num]
         if self._prop.classname == 'user':
             klass = HTMLUser
@@ -878,6 +1031,11 @@ class MultilinkHTMLProperty(HTMLProperty):
             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
         '''
@@ -890,6 +1048,8 @@ class MultilinkHTMLProperty(HTMLProperty):
         return [klass(self._client, self._prop.classname, value) for value in l]
 
     def plain(self, escape=0):
+        ''' Render a "plain" representation of the property
+        '''
         linkcl = self._db.classes[self._prop.classname]
         k = linkcl.labelprop(1)
         labels = []
@@ -901,12 +1061,16 @@ class MultilinkHTMLProperty(HTMLProperty):
         return value
 
     def field(self, size=30, showid=0):
+        ''' 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]
@@ -915,6 +1079,8 @@ class MultilinkHTMLProperty(HTMLProperty):
 
     def menu(self, size=None, height=None, showid=0, additional=[],
             **conditions):
+        ''' Render a form select list for this property
+        '''
         value = self._value
 
         # sort function
@@ -930,14 +1096,20 @@ class MultilinkHTMLProperty(HTMLProperty):
         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
         k = linkcl.labelprop(1)
         for optionid in options:
-            option = linkcl.get(optionid, k)
+            # get the option value, and if it's None use an empty string
+            option = linkcl.get(optionid, k) or ''
+
+            # figure if this option is selected
             s = ''
             if optionid in value or option in value:
                 s = 'selected '
+
+            # figure the label
             if showid:
                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
+            # truncate if it's too long
             if size is not None and len(lab) > size:
                 lab = lab[:size-3] + '...'
             if additional:
@@ -945,6 +1117,8 @@ class MultilinkHTMLProperty(HTMLProperty):
                 for propname in additional:
                     m.append(linkcl.get(optionid, propname))
                 lab = lab + ' (%s)'%', '.join(m)
+
+            # and generate
             lab = cgi.escape(lab)
             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
                 lab))
@@ -982,7 +1156,10 @@ 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
@@ -999,7 +1176,6 @@ class HTMLRequest:
 
         "form" the CGI form as a cgi.FieldStorage
         "env" the CGI environment variables
-        "url" the current URL path for this request
         "base" the base URL for this instance
         "user" a HTMLUser instance for this user
         "classname" the current classname (possibly None)
@@ -1023,13 +1199,17 @@ class HTMLRequest:
         self.form = client.form
         self.env = client.env
         self.base = client.base
-        self.url = client.url
         self.user = HTMLUser(client, 'user', client.userid)
 
         # store the current class name and action
         self.classname = client.classname
         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 = []
         if self.form.has_key(':columns'):
@@ -1091,7 +1271,25 @@ class HTMLRequest:
         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)
@@ -1099,12 +1297,17 @@ class HTMLRequest:
     def description(self):
         ''' Return a description of the request - handle for the page title.
         '''
-        s = [self.client.db.config.INSTANCE_NAME]
+        s = [self.client.db.config.TRACKER_NAME]
         if self.classname:
             if self.client.nodeid:
                 s.append('- %s%s'%(self.classname, self.client.nodeid))
             else:
-                s.append('- index of '+self.classname)
+                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)
@@ -1122,7 +1325,6 @@ class HTMLRequest:
         d['env'] = e
         return '''
 form: %(form)s
-url: %(url)r
 base: %(base)r
 classname: %(classname)r
 template: %(template)r
@@ -1166,7 +1368,7 @@ env: %(env)s
         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 and not args.has_key(':columns'):
@@ -1195,6 +1397,7 @@ env: %(env)s
         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 '''
@@ -1231,20 +1434,49 @@ function help_window(helpurl, width, height) {
             matches = None
         l = klass.filter(matches, filterspec, sort, group)
 
-        # return the batch object
-        return Batch(self.client, self.classname, l, self.pagesize,
-            self.startwith)
+        # map the item ids to instances
+        if self.classname == 'user':
+            klass = HTMLUser
+        else:
+            klass = HTMLItem
+        l = [klass(self.client, self.classname, item) for item in l]
 
+        # return the batch object
+        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):
@@ -1252,7 +1484,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)
@@ -1260,13 +1493,7 @@ class Batch(ZTUtils.Batch):
             self.last_item = self.current_item
             self.last_index = index
 
-        # wrap the return in an HTMLItem
-        if self.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-        self.current_item = klass(self.client, self.classname,
-            self._sequence[index+self.first])
+        self.current_item = self._sequence[index + self.first]
         return self.current_item
 
     def propchanged(self, property):
@@ -1282,7 +1509,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)
 
@@ -1291,10 +1518,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)