changeset 1911:f5c804379c85

fixed ZRoundup - mostly changes to classic template
author Richard Jones <richard@users.sourceforge.net>
date Wed, 12 Nov 2003 01:00:59 +0000
parents d19fd344bd1f
children 2b0ab61db194
files CHANGES.txt doc/upgrading.txt frontends/ZRoundup/ZRoundup.py roundup/backends/back_mysql.py roundup/backends/back_postgresql.py roundup/backends/back_sqlite.py roundup/backends/rdbms_common.py roundup/cgi/client.py roundup/cgi/templating.py templates/classic/html/_generic.help.html templates/classic/html/_generic.index.html templates/classic/html/_generic.item.html templates/classic/html/file.item.html templates/classic/html/issue.index.html templates/classic/html/issue.item.html templates/classic/html/issue.search.html templates/classic/html/keyword.item.html templates/classic/html/page.html templates/classic/html/user.index.html templates/classic/html/user.item.html templates/classic/html/user.register.html templates/classic/html/user.rego_progress.html templates/minimal/html/_generic.help.html templates/minimal/html/_generic.index.html templates/minimal/html/_generic.item.html templates/minimal/html/page.html templates/minimal/html/user.item.html
diffstat 27 files changed, 220 insertions(+), 395 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Tue Nov 11 22:37:25 2003 +0000
+++ b/CHANGES.txt	Wed Nov 12 01:00:59 2003 +0000
@@ -7,7 +7,8 @@
 - support setgid and running on port < 1024 (sf patch 777528)
 - using Zope3's test runner now, allowing GC checks, nicer controls and
   coverage analysis
-- added postgresql backend (originally from patch #761740, many changes since)
+- added postgresql backend (originally from sf patch 761740, many changes
+  since)
 - all RDBMS backends now have indexes on several columns
 - Change nosymessage and send_message to accept msgid=None (RFE #707235).
 
@@ -17,18 +18,19 @@
   couple of cases
 - HTML 4.01 validation on the 'classic' backend
 - Messages to the mailgw can be about classes other than issues now.
-- Signature matching is more precise (bug #827775).
-- Anonymous user can no longer edit or view itself (bug #828901).
-- Corrected typo in installation.html (bug #822967).
+- Signature matching is more precise (sf bug 827775).
+- Anonymous user can no longer edit or view itself (sf bug 828901).
+- Corrected typo in installation.html (sf bug 822967).
 - Clarified listTemplates docstring.
 - Print a nicer error message when the address is already in use 
-  (bug #798659).
+  (sf bug 798659).
 - Remove empty lines before sending strings off to the csv parser 
-  (bug #821364).
-- Centralised conversion of user-input data to hyperdb values (bug #802405,
-  bug #817217, rfe #816994)
+  (sf bug 821364).
+- Centralised conversion of user-input data to hyperdb values (sf bug 802405,
+  sf bug 817217, sf rfe 816994)
 - recalculate SHA on template files when installed tracker used as
   template (sf bug 827510)
+- fixed ZRoundup (sf bug 624380)
 
 Cleanup:
 - Replace curuserid attribute on Database with the extended getuid() method.
--- a/doc/upgrading.txt	Tue Nov 11 22:37:25 2003 +0000
+++ b/doc/upgrading.txt	Wed Nov 12 01:00:59 2003 +0000
@@ -21,6 +21,29 @@
 might still need to create an index "create index ids_name_idx on 
 ids(name)".
 
+0.7.0 ZRoundup changes
+----------------------
+
+The templates in your tracker's html directory will need updating if you
+wish to use ZRoundup. If you've not modified those files (or some of them),
+you may just copy the new versions from the Roundup source in the
+templates/classic/html directory.
+
+If you have modified the html files, then you'll need to manually edit them
+to change all occurances of special form variables from using the colon ":"
+special character to the at "@" special character. That is, variables such
+as::
+
+  :action :required :template :remove:messages ...
+
+should become:
+
+  @action @required @template @remove@messages ...
+
+Note that ``tal:`` statements are unaffected. So are TAL expression type
+prefixes such as ``python:`` and ``string:``. Please ask on the
+roundup-users mailing list for help if you're unsure.
+
 
 Migrating from 0.6.x to 0.6.3
 =============================
--- a/frontends/ZRoundup/ZRoundup.py	Tue Nov 11 22:37:25 2003 +0000
+++ b/frontends/ZRoundup/ZRoundup.py	Wed Nov 12 01:00:59 2003 +0000
@@ -14,7 +14,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: ZRoundup.py,v 1.16 2002-10-18 03:34:58 richard Exp $
+# $Id: ZRoundup.py,v 1.17 2003-11-12 01:00:58 richard Exp $
 #
 ''' ZRoundup module - exposes the roundup web interface to Zope
 
@@ -26,11 +26,6 @@
 independently of Zope. The roundup code is kept in memory though, and it
 runs in the same server as all your other Zope stuff, so it does have _some_
 advantages over regular CGI :)
-
-It also means that any requests which specify :filter, :columns or :sort
-_must_ be done using a GET, so that this interface can re-parse the
-QUERY_STRING. Zope interprets the ':' as a special character, and the special
-args are lost to it.
 '''
 
 import urlparse
@@ -142,16 +137,7 @@
             # the last element is the name
             env['TRACKER_NAME'] = path_components[-1]
 
-        if env['REQUEST_METHOD'] == 'GET':
-            # force roundup to re-parse the request because Zope fiddles
-            # with it and we lose all the :filter, :columns, etc goodness
-            form = None
-        else:
-            # For some reason, CRs are embeded in multiline notes.
-            # It doesn't occur with apache/roundup.cgi, though.
-            form = FormWrapper(self.REQUEST.form)
-
-        print (env['SCRIPT_NAME'], env['PATH_INFO'])
+        form = FormWrapper(self.REQUEST.form)
         return instance.Client(instance, request, env, form)
 
     security.declareProtected('View', 'index_html')
@@ -160,7 +146,7 @@
         '''
         # Redirect misdirected requests -- bugs 558867 , 565992
         # PATH_INFO, as defined by the CGI spec, has the *real* request path
-        orig_path = self.REQUEST.environ[ 'PATH_INFO' ]
+        orig_path = self.REQUEST.environ['PATH_INFO']
         if orig_path[-1] != '/' : 
             url = urlparse.urlparse( self.absolute_url() )
             url = list( url ) # make mutable
@@ -179,31 +165,30 @@
     def __getitem__(self, item):
         '''All other URL accesses are passed throuh to roundup
         '''
-        return PathElement(self, item)
+        return PathElement(self, item).__of__(self)
 
-class PathElement(Item, Implicit, Persistent):
-    def __init__(self, parent, path):
-        self.parent = parent
+class PathElement(Item, Implicit):
+    def __init__(self, zr, path):
+        self.zr = zr
         self.path = path
 
     def __getitem__(self, item):
         ''' Get a subitem.
         '''
-        return PathElement(self.path + '/' + item)
+        return PathElement(self.zr, self.path + '/' + item).__of__(self)
 
-    def __call__(self, *args, **kw):
+    def index_html(self, REQUEST=None):
         ''' Actually call through to roundup to handle the request.
         '''
-        print '*****', self.path
         try:
-            client = self.parent.roundup_opendb()
+            client = self.zr.roundup_opendb()
             # fake the path that roundup should use
             client.path = self.path
             # and call roundup to do something 
             client.main()
             return ''
         except NotFound:
-            raise 'NotFound', self.REQUEST.URL
+            raise 'NotFound', REQUEST.URL
             pass
         except:
             import traceback
--- a/roundup/backends/back_mysql.py	Tue Nov 11 22:37:25 2003 +0000
+++ b/roundup/backends/back_mysql.py	Wed Nov 12 01:00:59 2003 +0000
@@ -53,7 +53,7 @@
     mysql_backend = 'InnoDB'
     #mysql_backend = 'BDB'    # much slower, only use if you have no choice
     
-    def open_connection(self):
+    def sql_open_connection(self):
         db = getattr(self.config, 'MYSQL_DATABASE')
         try:
             self.conn = MySQLdb.connect(*db)
@@ -81,9 +81,6 @@
                 self.mysql_backend)
             self.sql("CREATE INDEX ids_name_idx on ids(name)")
 
-    def close(self):
-        self.conn.close()
-
     def __repr__(self):
         return '<myroundsql 0x%x>'%id(self)
 
--- a/roundup/backends/back_postgresql.py	Tue Nov 11 22:37:25 2003 +0000
+++ b/roundup/backends/back_postgresql.py	Wed Nov 12 01:00:59 2003 +0000
@@ -8,15 +8,15 @@
 # psycopg backend for roundup
 #
 
-from roundup.backends.rdbms_common import *
+from roundup import hyperdb, date
 from roundup.backends import rdbms_common
 import psycopg
 import os, shutil, popen2
 
-class Database(Database):
+class Database(rdbms_common.Database):
     arg = '%s'
 
-    def open_connection(self):
+    def sql_open_connection(self):
         db = getattr(self.config, 'POSTGRESQL_DATABASE')
         try:
             self.conn = psycopg.connect(**db)
@@ -33,18 +33,9 @@
             self.sql("CREATE TABLE schema (schema TEXT)")
             self.sql("CREATE TABLE ids (name VARCHAR(255), num INT4)")
 
-    def close(self):
-        self.conn.close()
-
     def __repr__(self):
         return '<roundpsycopgsql 0x%x>' % id(self)
 
-    def sql_fetchone(self):
-        return self.cursor.fetchone()
-
-    def sql_fetchall(self):
-        return self.cursor.fetchall()
-
     def sql_stringquote(self, value):
         ''' psycopg.QuotedString returns a "buffer" object with the
             single-quotes around it... '''
@@ -56,44 +47,6 @@
         self.cursor.execute(sql, (table_name, index_name))
         return self.cursor.fetchone()[0]
 
-    def save_dbschema(self, schema):
-        s = repr(self.database_schema)
-        self.sql('INSERT INTO schema VALUES (%s)', (s,))
-    
-    def load_dbschema(self):
-        self.cursor.execute('SELECT schema FROM schema')
-        schema = self.cursor.fetchone()
-        if schema:
-            return eval(schema[0])
-
-    def save_journal(self, classname, cols, nodeid, journaldate,
-                     journaltag, action, params):
-        params = repr(params)
-        entry = (nodeid, journaldate, journaltag, action, params)
-
-        a = self.arg
-        sql = 'INSERT INTO %s__journal (%s) values (%s, %s, %s, %s, %s)'%(
-            classname, cols, a, a, a, a, a)
-
-        if __debug__:
-          print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
-
-        self.cursor.execute(sql, entry)
-
-    def load_journal(self, classname, cols, nodeid):
-        sql = 'SELECT %s FROM %s__journal WHERE nodeid = %s' % (
-            cols, classname, self.arg)
-        
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getjournal', (self, sql, nodeid)
-
-        self.cursor.execute(sql, (nodeid,))
-        res = []
-        for nodeid, date_stamp, user, action, params in self.cursor.fetchall():
-            params = eval(params)
-            res.append((nodeid, date.Date(date_stamp), user, action, params))
-        return res
-
     def create_class_table(self, spec):
         cols, mls = self.determine_columns(spec.properties.items())
         cols.append('id')
@@ -126,79 +79,10 @@
 
         self.cursor.execute(sql)
 
-class PsycopgClass:
-    def find(self, **propspec):
-        """Get the ids of nodes in this class which link to the given nodes."""
-        
-        if __debug__:
-            print >>hyperdb.DEBUG, 'find', (self, propspec)
-
-        # shortcut
-        if not propspec:
-            return []
-
-        # validate the args
-        props = self.getprops()
-        propspec = propspec.items()
-        for propname, nodeids in propspec:
-            # check the prop is OK
-            prop = props[propname]
-            if not isinstance(prop, Link) and not isinstance(prop, Multilink):
-                raise TypeError, "'%s' not a Link/Multilink property"%propname
-
-        # first, links
-        l = []
-        where = []
-        allvalues = ()
-        a = self.db.arg
-        for prop, values in propspec:
-            if not isinstance(props[prop], hyperdb.Link):
-                continue
-            if type(values) is type(''):
-                allvalues += (values,)
-                where.append('_%s = %s' % (prop, a))
-            elif values is None:
-                where.append('_%s is NULL'%prop)
-            else:
-                allvalues += tuple(values.keys())
-                where.append('_%s in (%s)' % (prop, ','.join([a]*len(values))))
-        tables = []
-        if where:
-            self.db.sql('SELECT id AS nodeid FROM _%s WHERE %s' % (
-                self.classname, ' and '.join(where)), allvalues)
-            l += [x[0] for x in self.db.sql_fetchall()]
-
-        # now multilinks
-        for prop, values in propspec:
-            vals = ()
-            if not isinstance(props[prop], hyperdb.Multilink):
-                continue
-            if type(values) is type(''):
-                vals = (values,)
-                s = a
-            else:
-                vals = tuple(values.keys())
-                s = ','.join([a]*len(values))
-            query = 'SELECT nodeid FROM %s_%s WHERE linkid IN (%s)'%(
-                self.classname, prop, s)
-            self.db.sql(query, vals)
-            l += [x[0] for x in self.db.sql_fetchall()]
-            
-        if __debug__:
-            print >>hyperdb.DEBUG, 'find ... ', l
-
-        # Remove duplicated ids
-        d = {}
-        for k in l:
-            d[k] = 1
-        return d.keys()
-
-        return l
-
-class Class(PsycopgClass, rdbms_common.Class):
+class Class(rdbms_common.Class):
     pass
-class IssueClass(PsycopgClass, rdbms_common.IssueClass):
+class IssueClass(rdbms_common.IssueClass):
     pass
-class FileClass(PsycopgClass, rdbms_common.FileClass):
+class FileClass(rdbms_common.FileClass):
     pass
 
--- a/roundup/backends/back_sqlite.py	Tue Nov 11 22:37:25 2003 +0000
+++ b/roundup/backends/back_sqlite.py	Wed Nov 12 01:00:59 2003 +0000
@@ -1,17 +1,19 @@
-# $Id: back_sqlite.py,v 1.11 2003-11-11 11:19:18 richard Exp $
+# $Id: back_sqlite.py,v 1.12 2003-11-12 01:00:58 richard Exp $
 __doc__ = '''
 See https://pysqlite.sourceforge.net/ for pysqlite info
 '''
-import base64, marshal
-from roundup.backends.rdbms_common import *
+import os, base64, marshal
+
+from roundup import hyperdb
+from roundup.backends import rdbms_common
 from roundup.backends import locking
 import sqlite
 
-class Database(Database):
+class Database(rdbms_common.Database):
     # char to use for positional arguments
     arg = '%s'
 
-    def open_connection(self):
+    def sql_open_connection(self):
         # ensure files are group readable and writable
         os.umask(0002)
         db = os.path.join(self.config.DATABASE, 'db')
@@ -34,10 +36,8 @@
             self.cursor.execute('create table ids (name varchar, num integer)')
             self.cursor.execute('create index ids_name_idx on ids(name)')
 
-    def close(self):
-        ''' Close off the connection.
-
-            Squash any error caused by us already having closed the
+    def sql_close(self):
+        ''' Squash any error caused by us already having closed the
             connection.
         '''
         try:
@@ -46,55 +46,19 @@
             if str(value) != 'close failed - Connection is closed.':
                 raise
 
-        # release the lock too
-        if self.lockfile is not None:
-            locking.release_lock(self.lockfile)
-        if self.lockfile is not None:
-            self.lockfile.close()
-            self.lockfile = None
-
-    def rollback(self):
-        ''' Reverse all actions from the current transaction.
-
-            Undo all the changes made since the database was opened or the
-            last commit() or rollback() was performed.
-
-            Squash any error caused by us having closed the connection (and
+    def sql_rollback(self):
+        ''' Squash any error caused by us having closed the connection (and
             therefore not having anything to roll back)
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'rollback', (self,)
-
-        # roll back
         try:
             self.conn.rollback()
         except sqlite.ProgrammingError, value:
             if str(value) != 'rollback failed - Connection is closed.':
                 raise
 
-        # roll back "other" transaction stuff
-        for method, args in self.transactions:
-            # delete temporary files
-            if method == self.doStoreFile:
-                self.rollbackStoreFile(*args)
-        self.transactions = []
-
-        # clear the cache
-        self.clearCache()
-
     def __repr__(self):
         return '<roundlite 0x%x>'%id(self)
 
-    def sql_fetchone(self):
-        ''' Fetch a single row. If there's nothing to fetch, return None.
-        '''
-        return self.cursor.fetchone()
-
-    def sql_fetchall(self):
-        ''' Fetch a single row. If there's nothing to fetch, return [].
-        '''
-        return self.cursor.fetchall()
-
     def sql_commit(self):
         ''' Actually commit to the database.
 
@@ -113,86 +77,22 @@
                 return 1
         return 0
 
-    def save_dbschema(self, schema):
-        ''' Save the schema definition that the database currently implements
-        '''
-        s = repr(self.database_schema)
-        self.sql('insert into schema values (%s)', (s,))
-
-    def load_dbschema(self):
-        ''' Load the schema definition that the database currently implements
-        '''
-        self.cursor.execute('select schema from schema')
-        return eval(self.cursor.fetchone()[0])
-
-    def save_journal(self, classname, cols, nodeid, journaldate,
-            journaltag, action, params):
-        ''' Save the journal entry to the database
-        '''
-        # make the params db-friendly
-        params = repr(params)
-        entry = (nodeid, journaldate, journaltag, action, params)
-
-        # do the insert
-        a = self.arg
-        sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(classname,
-            cols, a, a, a, a, a)
-        if __debug__:
-            print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
-        self.cursor.execute(sql, entry)
-
-    def load_journal(self, classname, cols, nodeid):
-        ''' Load the journal from the database
+class sqliteClass:
+    def filter(self, search_matches, filterspec, sort=(None,None),
+            group=(None,None)):
+        ''' If there's NO matches to a fetch, sqlite returns NULL
+            instead of nothing
         '''
-        # now get the journal entries
-        sql = 'select %s from %s__journal where nodeid=%s'%(cols, classname,
-            self.arg)
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getjournal', (self, sql, nodeid)
-        self.cursor.execute(sql, (nodeid,))
-        res = []
-        for nodeid, date_stamp, user, action, params in self.cursor.fetchall():
-            params = eval(params)
-            res.append((nodeid, date.Date(date_stamp), user, action, params))
-        return res
+        return filter(None, rdbms_common.Class.filter(self, search_matches,
+            filterspec, sort=sort, group=group))
 
-    def unserialise(self, classname, node):
-        ''' Decode the marshalled node data
+class Class(sqliteClass, rdbms_common.Class):
+    pass
 
-            SQLite stringifies _everything_... so we need to re-numberificate
-            Booleans and Numbers.
-        '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'unserialise', classname, node
-        properties = self.getclass(classname).getprops()
-        d = {}
-        for k, v in node.items():
-            # if the property doesn't exist, or is the "retired" flag then
-            # it won't be in the properties dict
-            if not properties.has_key(k):
-                d[k] = v
-                continue
-
-            # get the property spec
-            prop = properties[k]
+class IssueClass(sqliteClass, rdbms_common.IssueClass):
+    pass
 
-            if isinstance(prop, Date) and v is not None:
-                d[k] = date.Date(v)
-            elif isinstance(prop, Interval) and v is not None:
-                d[k] = date.Interval(v)
-            elif isinstance(prop, Password) and v is not None:
-                p = password.Password()
-                p.unpack(v)
-                d[k] = p
-            elif isinstance(prop, Boolean) and v is not None:
-                d[k] = int(v)
-            elif isinstance(prop, Number) and v is not None:
-                # try int first, then assume it's a float
-                try:
-                    d[k] = int(v)
-                except ValueError:
-                    d[k] = float(v)
-            else:
-                d[k] = v
-        return d
+class FileClass(sqliteClass, rdbms_common.FileClass):
+    pass
 
+
--- a/roundup/backends/rdbms_common.py	Tue Nov 11 22:37:25 2003 +0000
+++ b/roundup/backends/rdbms_common.py	Wed Nov 12 01:00:59 2003 +0000
@@ -1,4 +1,4 @@
-# $Id: rdbms_common.py,v 1.67 2003-11-11 11:19:18 richard Exp $
+# $Id: rdbms_common.py,v 1.68 2003-11-12 01:00:58 richard Exp $
 ''' Relational database (SQL) backend common code.
 
 Basics:
@@ -69,13 +69,13 @@
         self.lockfile = None
 
         # open a connection to the database, creating the "conn" attribute
-        self.open_connection()
+        self.sql_open_connection()
 
     def clearCache(self):
         self.cache = {}
         self.cache_lru = []
 
-    def open_connection(self):
+    def sql_open_connection(self):
         ''' Open a connection to the database, creating it if necessary
         '''
         raise NotImplemented
@@ -93,7 +93,12 @@
     def sql_fetchone(self):
         ''' Fetch a single row. If there's nothing to fetch, return None.
         '''
-        raise NotImplemented
+        return self.cursor.fetchone()
+
+    def sql_fetchall(self):
+        ''' Fetch all rows. If there's nothing to fetch, return [].
+        '''
+        return self.cursor.fetchall()
 
     def sql_stringquote(self, value):
         ''' Quote the string so it's safe to put in the 'sql quotes'
@@ -103,12 +108,14 @@
     def save_dbschema(self, schema):
         ''' Save the schema definition that the database currently implements
         '''
-        raise NotImplemented
+        s = repr(self.database_schema)
+        self.sql('insert into schema values (%s)', (s,))
 
     def load_dbschema(self):
         ''' Load the schema definition that the database currently implements
         '''
-        raise NotImplemented
+        self.cursor.execute('select schema from schema')
+        return eval(self.cursor.fetchone()[0])
 
     def post_init(self):
         ''' Called once the schema initialisation has finished.
@@ -806,8 +813,14 @@
                 p = password.Password()
                 p.unpack(v)
                 d[k] = p
-            elif (isinstance(prop, Boolean) or isinstance(prop, Number)) and v is not None:
-                d[k]=float(v)
+            elif isinstance(prop, Boolean) and v is not None:
+                d[k] = int(v)
+            elif isinstance(prop, Number) and v is not None:
+                # try int first, then assume it's a float
+                try:
+                    d[k] = int(v)
+                except ValueError:
+                    d[k] = float(v)
             else:
                 d[k] = v
         return d
@@ -865,12 +878,6 @@
         self.save_journal(classname, cols, nodeid, journaldate,
             journaltag, action, params)
 
-    def save_journal(self, classname, cols, nodeid, journaldate,
-            journaltag, action, params):
-        ''' Save the journal entry to the database
-        '''
-        raise NotImplemented
-
     def getjournal(self, classname, nodeid):
         ''' get the journal for id
         '''
@@ -881,10 +888,36 @@
         cols = ','.join('nodeid date tag action params'.split())
         return self.load_journal(classname, cols, nodeid)
 
+    def save_journal(self, classname, cols, nodeid, journaldate,
+            journaltag, action, params):
+        ''' Save the journal entry to the database
+        '''
+        # make the params db-friendly
+        params = repr(params)
+        entry = (nodeid, journaldate, journaltag, action, params)
+
+        # do the insert
+        a = self.arg
+        sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(classname,
+            cols, a, a, a, a, a)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
+        self.cursor.execute(sql, entry)
+
     def load_journal(self, classname, cols, nodeid):
         ''' Load the journal from the database
         '''
-        raise NotImplemented
+        # now get the journal entries
+        sql = 'select %s from %s__journal where nodeid=%s'%(cols, classname,
+            self.arg)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'load_journal', (self, sql, nodeid)
+        self.cursor.execute(sql, (nodeid,))
+        res = []
+        for nodeid, date_stamp, user, action, params in self.cursor.fetchall():
+            params = eval(params)
+            res.append((nodeid, date.Date(date_stamp), user, action, params))
+        return res
 
     def pack(self, pack_before):
         ''' Delete all journal entries except "create" before 'pack_before'.
@@ -933,6 +966,9 @@
         # clear out the transactions
         self.transactions = []
 
+    def sql_rollback(self):
+        self.conn.rollback()
+
     def rollback(self):
         ''' Reverse all actions from the current transaction.
 
@@ -942,8 +978,7 @@
         if __debug__:
             print >>hyperdb.DEBUG, 'rollback', (self,)
 
-        # roll back
-        self.conn.rollback()
+        self.sql_rollback()
 
         # roll back "other" transaction stuff
         for method, args in self.transactions:
@@ -961,10 +996,13 @@
         # return the classname, nodeid so we reindex this content
         return (classname, nodeid)
 
+    def sql_close(self):
+        self.conn.close()
+
     def close(self):
         ''' Close off the connection.
         '''
-        self.conn.close()
+        self.sql_close()
         if self.lockfile is not None:
             locking.release_lock(self.lockfile)
         if self.lockfile is not None:
@@ -2061,12 +2099,10 @@
         else:
             # psycopg doesn't like empty args
             self.db.cursor.execute(sql)
-        l = self.db.cursor.fetchall()
+        l = self.db.sql_fetchall()
 
         # return the IDs (the first column)
-        # XXX The filter(None, l) bit is sqlite-specific... if there's _NO_
-        # XXX matches to a fetch, it returns NULL instead of nothing!?!
-        return filter(None, [row[0] for row in l])
+        return [row[0] for row in l]
 
     def count(self):
         '''Get the number of nodes in this class.
--- a/roundup/cgi/client.py	Tue Nov 11 22:37:25 2003 +0000
+++ b/roundup/cgi/client.py	Wed Nov 12 01:00:59 2003 +0000
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.144 2003-11-11 00:35:14 richard Exp $
+# $Id: client.py,v 1.145 2003-11-12 01:00:59 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -422,7 +422,7 @@
             else:
                 self.template = ''
             return
-        elif path[0] == '_file':
+        elif path[0] in ('_file', '@@file'):
             raise SendStaticFile, os.path.join(*path[1:])
         else:
             self.classname = path[0]
--- a/roundup/cgi/templating.py	Tue Nov 11 22:37:25 2003 +0000
+++ b/roundup/cgi/templating.py	Wed Nov 12 01:00:59 2003 +0000
@@ -469,14 +469,14 @@
         if property:
             property = '&amp;property=%s'%property
         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
-            ':startwith=0&amp;:template=help&amp;properties=%s%s\', \'%s\', \
+            '@startwith=0&amp;@template=help&amp;properties=%s%s\', \'%s\', \
             \'%s\')">%s</a>'%(self.classname, properties, property, width,
             height, label)
 
     def submit(self, label="Submit New Entry"):
         ''' Generate a submit button (and action hidden element)
         '''
-        return '  <input type="hidden" name=":action" value="new">\n'\
+        return '  <input type="hidden" name="@action" value="new">\n'\
         '  <input type="submit" name="submit" value="%s">'%label
 
     def history(self):
@@ -554,7 +554,7 @@
     def submit(self, label="Submit Changes"):
         ''' Generate a submit button (and action hidden element)
         '''
-        return '  <input type="hidden" name=":action" value="edit">\n'\
+        return '  <input type="hidden" name="@action" value="edit">\n'\
         '  <input type="submit" name="submit" value="%s">'%label
 
     def journal(self, direction='descending'):
@@ -773,7 +773,7 @@
         req.classname = self._klass.get(self._nodeid, 'klass')
         name = self._klass.get(self._nodeid, 'name')
         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
-            '&:queryname=%s'%urllib.quote(name))
+            '&@queryname=%s'%urllib.quote(name))
 
         # new template, using the specified classname and request
         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
@@ -961,9 +961,9 @@
     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 ":confirm:name".
+            a field with name "@confirm@name".
         '''
-        return '<input type="password" name=":confirm:%s" size="%s">'%(
+        return '<input type="password" name="@confirm@%s" size="%s">'%(
             self._formname, size)
 
 class NumberHTMLProperty(HTMLProperty):
--- a/templates/classic/html/_generic.help.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/_generic.help.html	Wed Nov 12 01:00:59 2003 +0000
@@ -1,7 +1,7 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html>
   <head>
-      <link rel="stylesheet" type="text/css" href="_file/style.css" />
+      <link rel="stylesheet" type="text/css" href="@@file/style.css" />
       <meta http-equiv="Content-Type" content="text/html; charset=utf-8;" />
       <tal:block tal:condition="python:request.form.has_key('property')">
       <title tal:content="string:${request/form/property/value} help">Property</title>
@@ -10,13 +10,13 @@
           // this is the name of the field in the original form that we're working on
           field = '${request/form/property/value}';" >
       </script>
-      <script src="_file/help_controls.js" type="text/javascript"><!-- 
+      <script src="@@file/help_controls.js" type="text/javascript"><!-- 
       //--></script>
       </tal:block>
   </head>
  <body class="body" onload="resetList();">
  <form name="frm_help" tal:attributes="action request/base"
-       tal:define="start python:int(request.form[':startwith'].value);
+       tal:define="start python:int(request.form['@startwith'].value);
                    batch python:utils.Batch(context.list(), 500, start);
                    props python:request.form['properties'].value.split(',')">
      
@@ -60,14 +60,14 @@
       <tr class="navigation">
        <th>
         <a tal:define="prev batch/previous" tal:condition="prev"
-           tal:attributes="href string:${request/classname}?:template=help&:startwith=${prev/first}&properties=${request/form/properties/value}">&lt;&lt; previous</a>
+           tal:attributes="href string:${request/classname}?@template=help&@startwith=${prev/first}&properties=${request/form/properties/value}">&lt;&lt; previous</a>
         &nbsp;
        </th>
        <th tal:content="python: '%d...%d out of %d'%(batch.start,
                batch.start+batch.length-1, batch.sequence_length)">current</th>
        <th>
         <a tal:define="next batch/next" tal:condition="next"
-           tal:attributes="href string:${request/classname}?:template=help&:startwith=${next/first}&properties=${request/form/properties/value}">next &gt;&gt;</a>
+           tal:attributes="href string:${request/classname}?@template=help&@startwith=${next/first}&properties=${request/form/properties/value}">next &gt;&gt;</a>
         &nbsp;
        </th>
       </tr>
--- a/templates/classic/html/_generic.index.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/_generic.index.html	Wed Nov 12 01:00:59 2003 +0000
@@ -35,7 +35,7 @@
       tal:attributes="action context/designator">
 <textarea rows="15" cols="60" name="rows" tal:content="context/csv"></textarea>
 <br>
-<input type="hidden" name=":action" value="editCSV">
+<input type="hidden" name="@action" value="editCSV">
 <input type="submit" value="Edit Items">
 </form>
 </tal:block>
--- a/templates/classic/html/_generic.item.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/_generic.item.html	Wed Nov 12 01:00:59 2003 +0000
@@ -14,8 +14,7 @@
       enctype="multipart/form-data" tal:condition="context/is_edit_ok"
       tal:attributes="action context/designator">
 
-<input type="hidden" name=":template" value="item">
-<input type="hidden" name=":required" value="title">
+<input type="hidden" name="@template" value="item">
 
 <table class="form">
 
--- a/templates/classic/html/file.item.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/file.item.html	Wed Nov 12 01:00:59 2003 +0000
@@ -27,11 +27,11 @@
  <tr>
   <td>
    &nbsp;
-   <input type="hidden" name=":template" value="item">
-   <input type="hidden" name=":required" value="name,type">
-   <input type="hidden" name=":multilink"
-          tal:condition="python:request.form.has_key(':multilink')"
-          tal:attributes="value request/form/:multilink/value">
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="name,type">
+   <input type="hidden" name="@multilink"
+          tal:condition="python:request.form.has_key('@multilink')"
+          tal:attributes="value request/form/@multilink/value">
   </td>
   <td tal:content="structure context/submit">submit button here</td>
  </tr>
--- a/templates/classic/html/issue.index.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/issue.index.html	Wed Nov 12 01:00:59 2003 +0000
@@ -64,7 +64,7 @@
      <th>
       <a tal:define="prev batch/previous" tal:condition="prev"
          tal:attributes="href python:request.indexargs_href(request.classname,
-         {':startwith':prev.first, ':pagesize':prev.size})">&lt;&lt; previous</a>
+         {'@startwith':prev.first, '@pagesize':prev.size})">&lt;&lt; previous</a>
       &nbsp;
      </th>
      <th tal:content="python: '%d...%d out of %d'%(batch.start,
@@ -72,7 +72,7 @@
      <th>
       <a tal:define="next batch/next" tal:condition="next"
          tal:attributes="href python:request.indexargs_href(request.classname,
-         {':startwith':next.first, ':pagesize':next.size})">next &gt;&gt;</a>
+         {'@startwith':next.first, '@pagesize':next.size})">next &gt;&gt;</a>
       &nbsp;
      </th>
     </tr>
@@ -87,7 +87,7 @@
   <tr tal:condition="batch">
    <th>Sort on:</th>
    <td>
-    <select name=":sort">
+    <select name="@sort">
      <option value="">- nothing -</option>
      <option tal:repeat="col context/properties"
              tal:attributes="value col/_name;
@@ -96,14 +96,14 @@
     </select>
    </td>
    <th>Descending:</th>
-   <td><input type="checkbox" name=":sortdir"
+   <td><input type="checkbox" name="@sortdir"
               tal:attributes="checked python:request.sort[0] == '-'"> 
    </td>
   </tr>
   <tr>
    <th>Group on:</th>
    <td>
-    <select name=":group">
+    <select name="@group">
      <option value="">- nothing -</option>
      <option tal:repeat="col context/properties"
              tal:attributes="value col/_name;
@@ -112,7 +112,7 @@
     </select>
    </td>
    <th>Descending:</th>
-   <td><input type="checkbox" name=":groupdir"
+   <td><input type="checkbox" name="@groupdir"
               tal:attributes="checked python:request.group[0] == '-'"> 
    </td>
   </tr>
@@ -128,4 +128,3 @@
 
 </td>
 </tal:block>
-
--- a/templates/classic/html/issue.item.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/issue.item.html	Wed Nov 12 01:00:59 2003 +0000
@@ -67,21 +67,21 @@
 <tr>
  <th>Change Note</th>
  <td colspan=3>
-  <textarea tal:content="request/form/:note/value | default"
-            name=":note" wrap="hard" rows="5" cols="80"></textarea>
+  <textarea tal:content="request/form/@note/value | default"
+            name="@note" wrap="hard" rows="5" cols="80"></textarea>
  </td>
 </tr>
 
 <tr>
  <th>File</th>
- <td colspan=3><input type="file" name=":file" size="40"></td>
+ <td colspan=3><input type="file" name="@file" size="40"></td>
 </tr>
 
 <tr>
  <td>
   &nbsp;
-  <input type="hidden" name=":template" value="item">
-  <input type="hidden" name=":required" value="title,priority">
+  <input type="hidden" name="@template" value="item">
+  <input type="hidden" name="@required" value="title,priority">
  </td>
  <td colspan=3 tal:content="structure context/submit">
   submit button will go here
@@ -142,7 +142,7 @@
     <th tal:content="string:Date: ${msg/date}">date</th>
     <th>
      <a tal:condition="context/is_edit_ok"
-        tal:attributes="href string:issue${context/id}?:remove:messages=${msg/id}&:action=edit">remove</a>
+        tal:attributes="href string:issue${context/id}?@remove@messages=${msg/id}&@action=edit">remove</a>
     </th>
    </tr>
    <tr>
--- a/templates/classic/html/issue.search.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/issue.search.html	Wed Nov 12 01:00:59 2003 +0000
@@ -26,7 +26,7 @@
  <th class="header">Group on</th>
 </tr>
 
-<tr tal:define="name string::search_text">
+<tr tal:define="name string:@search_text">
   <th>All text*:</th>
   <td metal:use-macro="search_input"></td>
   <td>&nbsp;</td>
@@ -136,40 +136,40 @@
 
 <tr>
 <th>Pagesize:</th>
-<td><input name=":pagesize" size="3" value="50"
-           tal:attributes="value request/form/:pagesize/value | default"></td>
+<td><input name="@pagesize" size="3" value="50"
+           tal:attributes="value request/form/@pagesize/value | default"></td>
 </tr>
 
 <tr>
 <th>Start With:</th>
-<td><input name=":startwith" size="3" value="0"
-           tal:attributes="value request/form/:startwith/value | default"></td>
+<td><input name="@startwith" size="3" value="0"
+           tal:attributes="value request/form/@startwith/value | default"></td>
 </tr>
 
 <tr>
 <th>Sort Descending:</th>
-<td><input type="checkbox" name=":sortdir"
+<td><input type="checkbox" name="@sortdir"
            tal:attributes="checked python:request.sort[0] == '-' or request.sort[0] is None">
 </td>
 </tr>
 
 <tr>
 <th>Group Descending:</th>
-<td><input type="checkbox" name=":groupdir"
+<td><input type="checkbox" name="@groupdir"
            tal:attributes="checked python:request.group[0] == '-'">
 </td>
 </tr>
 
 <tr>
 <th>Query name**:</th>
-<td><input name=":queryname"
-           tal:attributes="value request/form/:queryname/value | default"></td>
+<td><input name="@queryname"
+           tal:attributes="value request/form/@queryname/value | default"></td>
 </tr>
 
 <tr>
   <td>
    &nbsp;
-   <input type="hidden" name=":action" value="search">
+   <input type="hidden" name="@action" value="search">
   </td>
   <td><input type="submit" value="Search"></td>
 </tr>
--- a/templates/classic/html/keyword.item.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/keyword.item.html	Wed Nov 12 01:00:59 2003 +0000
@@ -41,8 +41,8 @@
   <tr>
    <td>
     &nbsp;
-    <input type="hidden" name=":required" value="name">
-    <input type="hidden" name=":template" value="item">
+    <input type="hidden" name="@required" value="name">
+    <input type="hidden" name="@template" value="item">
    </td>
    <td colspan=3 tal:content="structure context/submit">
     submit button will go here
--- a/templates/classic/html/page.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/page.html	Wed Nov 12 01:00:59 2003 +0000
@@ -6,7 +6,7 @@
 <title metal:define-slot="head_title">title goes here</title>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8;">
 
-<link rel="stylesheet" type="text/css" href="_file/style.css">
+<link rel="stylesheet" type="text/css" href="@@file/style.css">
 
 <script tal:replace="structure request/base_javascript">
 </script>
@@ -36,13 +36,13 @@
        tal:condition="python:request.user.hasPermission('View', 'issue')">
     <b>Issues</b><br>
     <a tal:condition="python:request.user.hasPermission('Edit', 'issue')"
-      href="issue?:template=item">Create New<br></a>
-    <a href="issue?:sort=-activity&:group=priority&:filter=status,assignedto&:columns=id,activity,title,creator,status&status=-1,1,2,3,4,5,6,7&assignedto=-1">Show Unassigned</a><br>
-    <a href="issue?:sort=-activity&:group=priority&:filter=status&:columns=id,activity,title,creator,assignedto,status&status=-1,1,2,3,4,5,6,7">Show All</a><br>
-    <a href="issue?:template=search">Search</a><br>
-    <input type="submit" style="padding: 0" value="Show issue:"><input size="4" type="text" name=":number">
-    <input type="hidden" name=":type" value="issue">
-    <input type="hidden" name=":action" value="show">
+      href="issue?@template=item">Create New<br></a>
+    <a href="issue?@sort=-activity&@group=priority&@filter=status,assignedto&@columns=id,activity,title,creator,status&status=-1,1,2,3,4,5,6,7&assignedto=-1">Show Unassigned</a><br>
+    <a href="issue?@sort=-activity&@group=priority&@filter=status&@columns=id,activity,title,creator,assignedto,status&status=-1,1,2,3,4,5,6,7">Show All</a><br>
+    <a href="issue?@template=search">Search</a><br>
+    <input type="submit" style="padding: 0" value="Show issue:"><input size="4" type="text" name="@number">
+    <input type="hidden" name="@type" value="issue">
+    <input type="hidden" name="@action" value="show">
    </p>
   </form>
 
@@ -50,23 +50,23 @@
      tal:condition="python:request.user.hasPermission('View', 'keyword')">
    <b>Keywords</b><br>
    <a tal:condition="python:request.user.hasPermission('Edit', 'keyword')"
-      href="keyword?:template=item">Create New<br></a>
+      href="keyword?@template=item">Create New<br></a>
    <a tal:condition="python:request.user.hasPermission('Edit', 'keyword') and
                             len(db.keyword.list())"
-      href="keyword?:template=item">Edit Existing<br></a>
+      href="keyword?@template=item">Edit Existing<br></a>
   </p>
 
   <p class="classblock"
        tal:condition="python:request.user.username != 'anonymous'">
    <b>Administration</b><br>
    <tal:block tal:condition="python:request.user.hasPermission('Edit', None)">
-    <a href="home?:template=classlist">Class List</a><br>
+    <a href="home?@template=classlist">Class List</a><br>
    </tal:block>
    <a tal:condition="python:request.user.hasPermission('View', 'user')
                             or request.user.hasPermission('Edit', 'user')"
       href="user" >User List</a><br>
    <a tal:condition="python:request.user.hasPermission('Edit', 'user')"
-      href="user?:template=item">Add User</a>
+      href="user?@template=item">Add User</a>
   </p>
 
   <form method="POST" tal:condition="python:request.user.username=='anonymous'"
@@ -75,20 +75,20 @@
     <b>Login</b><br>
     <input size="10" name="__login_name"><br>
     <input size="10" type="password" name="__login_password"><br>
-    <input type="submit" name=":action" value="Login"><br>
+    <input type="submit" name="@action" value="Login"><br>
     <span tal:replace="structure request/indexargs_form" />
-    <a href="user?:template=register"
+    <a href="user?@template=register"
        tal:condition="python:request.user.hasPermission('Web Registration')">Register<br></a>
-    <a href="user?:template=forgotten">Lost&nbsp;your&nbsp;login?</a><br>
+    <a href="user?@template=forgotten">Lost&nbsp;your&nbsp;login?</a><br>
    </p>
   </form>
    
   <p class="userblock" tal:condition="python:request.user.username != 'anonymous'">
    <b>Hello,</b> <b tal:content="request/user/username">username</b><br>
-   <a tal:attributes="href string:issue?:sort=-activity&:group=priority&:filter=status,assignedto&:columns=id,activity,title,creator,status&status=-1,1,2,3,4,5,6,7&assignedto=${request/user/id}">My Issues</a><br>
+   <a tal:attributes="href string:issue?@sort=-activity&@group=priority&@filter=status,assignedto&@columns=id,activity,title,creator,status&status=-1,1,2,3,4,5,6,7&assignedto=${request/user/id}">My Issues</a><br>
    <a tal:attributes="href string:user${request/user/id}">My Details</a><br>
    <a tal:attributes="href python:request.indexargs_href('',
-       {':action':'logout'})">Logout</a>
+       {'@action':'logout'})">Logout</a>
   </p>
   <p class="userblock">
    <b>Help</b><br>
@@ -133,19 +133,19 @@
 </td>
 
 <td metal:define-macro="column_input">
-  <input type="checkbox" name=":columns"
+  <input type="checkbox" name="@columns"
          tal:attributes="value name;
                          checked python:name in cols">
 </td>
 
 <td metal:define-macro="sort_input">
-  <input type="radio" name=":sort"
+  <input type="radio" name="@sort"
          tal:attributes="value name;
                          checked python:name == sort_on">
 </td>
 
 <td metal:define-macro="group_input">
-  <input type="radio" name=":group"
+  <input type="radio" name="@group"
          tal:attributes="value name;
                          checked python:name == group_on">
 </td>
--- a/templates/classic/html/user.index.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/user.index.html	Wed Nov 12 01:00:59 2003 +0000
@@ -30,7 +30,7 @@
  <td tal:content="python:user.address.email() or default">&nbsp;</td>
  <td tal:content="python:user.phone.plain() or default">&nbsp;</td>
  <td tal:condition="context/is_edit_ok">
-  <a tal:attributes="href string:user${user/id}?:action=retire&:template=index">
+  <a tal:attributes="href string:user${user/id}?@action=retire&@template=index">
    retire</a>
  </td>
 </tr>
--- a/templates/classic/html/user.item.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/user.item.html	Wed Nov 12 01:00:59 2003 +0000
@@ -72,8 +72,8 @@
  <tr>
   <td>
    &nbsp;
-   <input type="hidden" name=":template" value="item">
-   <input type="hidden" name=":required" value="username,address">
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="username,address">
   </td>
   <td tal:content="structure context/submit">submit button here</td>
  </tr>
@@ -90,7 +90,7 @@
    <a tal:attributes="href string:${query/klass}?${query/url}">display</a>   
   </td>
   <td>
-   <a tal:attributes="href string:?:remove:queries=${query/id}&:action=edit">remove</a>
+   <a tal:attributes="href string:?@remove@queries=${query/id}&@action=edit">remove</a>
   </td>
  </tr>
 </table>
--- a/templates/classic/html/user.register.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/user.register.html	Wed Nov 12 01:00:59 2003 +0000
@@ -64,9 +64,9 @@
  <tr>
   <td>&nbsp;</td>
   <td>
-   <input type="hidden" name=":template" value="register">
-   <input type="hidden" name=":required" value="username,password,address">
-   <input type="hidden" name=":action" value="register">
+   <input type="hidden" name="@template" value="register">
+   <input type="hidden" name="@required" value="username,password,address">
+   <input type="hidden" name="@action" value="register">
    <input type="submit" name="submit" value="Register">
   </td>
  </tr>
--- a/templates/classic/html/user.rego_progress.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/classic/html/user.rego_progress.html	Wed Nov 12 01:00:59 2003 +0000
@@ -13,4 +13,3 @@
 
 </td>
 </tal:block>
-
--- a/templates/minimal/html/_generic.help.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/minimal/html/_generic.help.html	Wed Nov 12 01:00:59 2003 +0000
@@ -16,7 +16,7 @@
   </head>
  <body class="body" marginwidth="0" marginheight="0" onload="resetList();">
  <form name="frm_help" action=""
-       tal:define="start python:int(request.form[':startwith'].value);
+       tal:define="start python:int(request.form['@startwith'].value);
                    batch python:utils.Batch(context.list(), 500, start);
                    props python:request.form['properties'].value.split(',')">
      
@@ -59,14 +59,14 @@
       <tr class="navigation">
        <th>
         <a tal:define="prev batch/previous" tal:condition="prev"
-           tal:attributes="href string:${request/classname}?:template=help&:startwith=${prev/first}&properties=${request/form/properties/value}">&lt;&lt; previous</a>
+           tal:attributes="href string:${request/classname}?@template=help&@startwith=${prev/first}&properties=${request/form/properties/value}">&lt;&lt; previous</a>
         &nbsp;
        </th>
        <th tal:content="python: '%d...%d out of %d'%(batch.start,
                batch.start+batch.length-1, batch.sequence_length)">current</th>
        <th>
         <a tal:define="next batch/next" tal:condition="next"
-           tal:attributes="href string:${request/classname}?:template=help&:startwith=${next/first}&properties=${request/form/properties/value}">next &gt;&gt;</a>
+           tal:attributes="href string:${request/classname}?@template=help&@startwith=${next/first}&properties=${request/form/properties/value}">next &gt;&gt;</a>
         &nbsp;
        </th>
       </tr>
--- a/templates/minimal/html/_generic.index.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/minimal/html/_generic.index.html	Wed Nov 12 01:00:59 2003 +0000
@@ -34,7 +34,7 @@
 <form onSubmit="return submit_once()" method="POST">
 <textarea rows="15" cols="60" name="rows" tal:content="context/csv"></textarea>
 <br>
-<input type="hidden" name=":action" value="editCSV">
+<input type="hidden" name="@action" value="editCSV">
 <input type="submit" value="Edit Items">
 </form>
 </tal:block>
--- a/templates/minimal/html/_generic.item.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/minimal/html/_generic.item.html	Wed Nov 12 01:00:59 2003 +0000
@@ -13,8 +13,7 @@
 <form method="POST" onSubmit="return submit_once()"
       enctype="multipart/form-data" tal:condition="context/is_edit_ok">
 
-<input type="hidden" name=":template" value="item">
-<input type="hidden" name=":required" value="title">
+<input type="hidden" name="@template" value="item">
 
 <table class="form">
 
--- a/templates/minimal/html/page.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/minimal/html/page.html	Wed Nov 12 01:00:59 2003 +0000
@@ -6,7 +6,7 @@
 <title metal:define-slot="head_title">title goes here</title>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8;">
 
-<link rel="stylesheet" type="text/css" href="_file/style.css">
+<link rel="stylesheet" type="text/css" href="@@file/style.css">
 
 <script tal:replace="structure request/base_javascript">
 </script>
@@ -27,30 +27,30 @@
    <form method="POST" action="">
     <input size="10" name="__login_name"><br>
     <input size="10" type="password" name="__login_password"><br>
-    <input type="submit" name=":action" value="login">
+    <input type="submit" name="@action" value="login">
     <span tal:replace="structure request/indexargs_form" />
    </form>
    <a tal:condition="python:request.user.hasPermission('Web Registration')"
-      href="user?:template=register">Register</a>
+      href="user?@template=register">Register</a>
   </p>
 
   <p class="userblock" tal:condition="python:request.user.username != 'anonymous'">
    <b>Hello,</b><br><b tal:content="request/user/username">username</b><br>
    <a tal:attributes="href string:user${request/user/id}">My Details</a><br>
    <a tal:attributes="href python:request.indexargs_href('',
-       {':action':'logout'})">Logout</a>
+       {'@action':'logout'})">Logout</a>
   </p>
 
   <p class="classblock"
        tal:condition="python:request.user.username != 'anonymous'">
    <b>Administration</b><br>
    <a tal:condition="python:request.user.hasPermission('Edit', None)"
-      href="home?:template=classlist">Class List</a><br>
+      href="home?@template=classlist">Class List</a><br>
    <a tal:condition="python:request.user.hasPermission('View', 'user')
                             or request.user.hasPermission('Edit', 'user')"
       href="user" >User List</a><br>
    <a tal:condition="python:request.user.hasPermission('Edit', 'user')"
-      href="user?:template=item">Add User</a>
+      href="user?@template=item">Add User</a>
   </p>
  </td>
  <td>
--- a/templates/minimal/html/user.item.html	Tue Nov 11 22:37:25 2003 +0000
+++ b/templates/minimal/html/user.item.html	Wed Nov 12 01:00:59 2003 +0000
@@ -12,7 +12,6 @@
 <form method="POST" onSubmit="return submit_once()"
       enctype="multipart/form-data" tal:condition="context/is_edit_ok">
 
-<input type="hidden" name=":required" value="username,address">
 
 <table class="form">
  <tr>
@@ -45,7 +44,10 @@
  </tr>
 
  <tr>
-  <td>&nbsp;</td>
+  <td>&nbsp;
+   <input type="hidden" name="@required" value="username,address">
+   <input type="hidden" name="@template" value="item">
+  </td>
   <td tal:content="structure context/submit">submit button here</td>
  </tr>
 </table>

Roundup Issue Tracker: http://roundup-tracker.org/