Code

Added the "actor" property. Metakit backend not done (still not confident
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Mon, 15 Mar 2004 05:50:20 +0000 (05:50 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Mon, 15 Mar 2004 05:50:20 +0000 (05:50 +0000)
I know how it's supposed to work ;)

Currently it will come up as NULL in the RDBMS backends for older items.
The *dbm backends will look up the journal. I hope to remedy the former
before 0.7's release.

Fixed a bunch of migration issues in the rdbms backends while I was at it
(index changes for key prop changes) and simplified the class table update
code for RDBMSes that have "alter table" in their command set (ie. not
sqlite) ... migration from "version 1" to "version 2" still hasn't
actually been tested yet though.

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@2147 57a73879-2fb5-44c3-a270-3262357dd7e2

14 files changed:
CHANGES.txt
TODO.txt
doc/upgrading.txt
roundup/backends/back_anydbm.py
roundup/backends/back_mysql.py
roundup/backends/back_postgresql.py
roundup/backends/back_sqlite.py
roundup/backends/rdbms_common.py
roundup/roundupdb.py
templates/classic/dbinit.py
templates/classic/html/issue.index.html
templates/classic/html/issue.item.html
templates/classic/html/issue.search.html
test/db_test_base.py

index 4152d2f9b1d977ccba16b6cb0117713e836d8785..710b1046440ff7321adde6fc19d28f01a933969a 100644 (file)
@@ -3,6 +3,8 @@ are given with the most recent entry first.
 
 200?-??-?? 0.7.0
 Feature:
+- added new "actor" automatic property (indicates user who cause the last
+  "activity"
 - simple support for collision detection (sf rfe 648763)
 - support confirming registration by replying to the email (sf bug 763668)
 - support setgid and running on port < 1024 (sf patch 777528)
index f300d4fa255a5a968b02b91d475cca0eab3f54d3..11211bb0782f6ef7c6d48239e93e64e140451359 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
@@ -3,8 +3,8 @@ doing before the next release:
 
 - sessions, otks and indexing in RDBMSes
 - add tests for group-by-multilink so I finally implement it for the RDBMSes
-- full coverage analysis for unit tests
 - s/getnode/getitem in backends (and s/Node/Item)
-- activity_by meta-property
+- have rdbms backends look up the journal for actor if it's not set
+- ensure index creation is triggered by the version 1->2 update
 
 - migrate to numeric ID values (fixes bug 817217)
index 47cd0216fea9a6ac4856507158bd5ed9dbbd388c..b53a457e08760c879c12b1511cb8ea6c87581d35 100644 (file)
@@ -51,6 +51,13 @@ add::
         p = db.security.getPermission('View', cl)
         db.security.addPermissionToRole('User', p)
 
+0.7.0 New "actor" property
+--------------------------
+
+Roundup's database has a new per-item property "actor" which reflects the
+user performing the last "actvitiy". See the classic template for ways to
+integrate this new property into your interface.
+
 
 0.7.0 Extending the cgi interface
 ---------------------------------
index 4d3ad98387e3298a1038f59f9f445b44cea6b642..64c6236aa3cb8ea66e40b5c8242bcfd48baf9750 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.136 2004-03-12 05:36:26 richard Exp $
+#$Id: back_anydbm.py,v 1.137 2004-03-15 05:50:20 richard Exp $
 '''This module defines a backend that saves the hyperdatabase in a
 database chosen by anydbm. It is guaranteed to always be available in python
 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
@@ -264,6 +264,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             # calling code's node assumptions)
             node = node.copy()
             node['creator'] = self.getuid()
+            node['actor'] = self.getuid()
             node['creation'] = node['activity'] = date.Date()
 
         self.newnodes.setdefault(classname, {})[nodeid] = 1
@@ -281,6 +282,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # calling code's node assumptions)
         node = node.copy()
         node['activity'] = date.Date()
+        node['actor'] = self.getuid()
 
         # can't set without having already loaded the node
         self.cache[classname][nodeid] = node
@@ -731,10 +733,10 @@ class Class(hyperdb.Class):
         or a ValueError is raised.  The keyword arguments in 'properties'
         must map names to property objects, or a TypeError is raised.
         '''
-        if (properties.has_key('creation') or properties.has_key('activity')
-                or properties.has_key('creator')):
-            raise ValueError, '"creation", "activity" and "creator" are '\
-                'reserved'
+        for name in 'creation activity creator actor'.split():
+            if properties.has_key(name):
+                raise ValueError, '"creation", "activity", "creator" and '\
+                    '"actor" are reserved'
 
         self.classname = classname
         self.properties = properties
@@ -1010,6 +1012,8 @@ class Class(hyperdb.Class):
             creation = None
         if d.has_key('activity'):
             del d['activity']
+        if d.has_key('actor'):
+            del d['actor']
         self.db.addjournal(self.classname, newid, 'create', {}, creator,
             creation)
         return newid
@@ -1063,7 +1067,27 @@ class Class(hyperdb.Class):
             journal = self.db.getjournal(self.classname, nodeid)
             if journal:
                 num_re = re.compile('^\d+$')
-                value = self.db.getjournal(self.classname, nodeid)[0][2]
+                value = journal[0][2]
+                if num_re.match(value):
+                    return value
+                else:
+                    # old-style "username" journal tag
+                    try:
+                        return self.db.user.lookup(value)
+                    except KeyError:
+                        # user's been retired, return admin
+                        return '1'
+            else:
+                return self.db.getuid()
+        if propname == 'actor':
+            if d.has_key('actor'):
+                return d['actor']
+            if not self.do_journal:
+                raise ValueError, 'Journalling is disabled for this class'
+            journal = self.db.getjournal(self.classname, nodeid)
+            if journal:
+                num_re = re.compile('^\d+$')
+                value = journal[-1][2]
                 if num_re.match(value):
                     return value
                 else:
@@ -1901,6 +1925,7 @@ class Class(hyperdb.Class):
             d['creation'] = hyperdb.Date()
             d['activity'] = hyperdb.Date()
             d['creator'] = hyperdb.Link('user')
+            d['actor'] = hyperdb.Link('user')
         return d
 
     def addprop(self, **properties):
index b5ffa5a394bd9eed7013c8026c89e6e709d3bba3..a02b445ec99b427811742f8c40e877fbe0f6ad22 100644 (file)
@@ -128,6 +128,13 @@ class Database(Database):
             's_last_use FLOAT(20), s_user VARCHAR(255))')
         self.cursor.execute('CREATE INDEX sessions_key_idx ON sessions(s_key)')
 
+    def add_actor_column(self):
+        # update existing tables to have the new actor column
+        tables = self.database_schema['tables']
+        for name in tables.keys():
+            self.cursor.execute('ALTER TABLE _%s add __actor '
+                'VARCHAR(255)'%name)
+
     def __repr__(self):
         return '<myroundsql 0x%x>'%id(self)
 
@@ -241,6 +248,16 @@ class Database(Database):
                 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
             self.cursor.execute(index_sql)
 
+    def drop_class_table_key_index(self, cn, key):
+        table_name = '_%s'%cn
+        index_name = '_%s_%s_idx'%(cn, key)
+        if not self.sql_index_exists(table_name, index_name):
+            return
+        sql = 'drop index %s on %s'%(index_name, table_name)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'drop_index', (self, sql)
+        self.cursor.execute(sql)
+
 class MysqlClass:
     # we're overriding this method for ONE missing bit of functionality.
     # look for "I can't believe it's not a toy RDBMS" below
index c58505658b00db012035d3b518cd39555f8c0608..fa9ba09e9de69ecda1807bd56ae4450f7a2c3d79 100644 (file)
@@ -115,6 +115,13 @@ class Database(rdbms_common.Database):
             's_last_use FLOAT(20), s_user VARCHAR(255))')
         self.cursor.execute('CREATE INDEX sessions_key_idx ON sessions(s_key)')
 
+    def add_actor_column(self):
+        # update existing tables to have the new actor column
+        tables = self.database_schema['tables']
+        for name in tables.keys():
+            self.cursor.execute('ALTER TABLE _%s add __actor '
+                'VARCHAR(255)'%name)
+
     def __repr__(self):
         return '<roundpsycopgsql 0x%x>' % id(self)
 
@@ -140,6 +147,7 @@ class Database(rdbms_common.Database):
             print >>hyperdb.DEBUG, 'create_class', (self, sql)
 
         self.cursor.execute(sql)
+        self.create_class_table_indexes(spec)
         return cols, mls
 
     def create_journal_table(self, spec):
@@ -151,6 +159,7 @@ class Database(rdbms_common.Database):
             print >>hyperdb.DEBUG, 'create_class', (self, sql)
 
         self.cursor.execute(sql)
+        self.create_journal_table_indexes(spec)
 
     def create_multilink_table(self, spec, ml):
         sql = '''CREATE TABLE "%s_%s" (linkid VARCHAR(255),
@@ -160,6 +169,7 @@ class Database(rdbms_common.Database):
             print >>hyperdb.DEBUG, 'create_class', (self, sql)
 
         self.cursor.execute(sql)
+        self.create_multilink_table_indexes(spec, ml)
 
 class Class(rdbms_common.Class):
     pass
index 1f1013c285c9302cf373bacea0e6927b38931c13..04100d09316daac2229e5979670148f0cbdc4b30 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: back_sqlite.py,v 1.15 2004-03-12 04:08:59 richard Exp $
+# $Id: back_sqlite.py,v 1.16 2004-03-15 05:50:20 richard Exp $
 '''Implements a backend for SQLite.
 
 See https://pysqlite.sourceforge.net/ for pysqlite info
@@ -48,6 +48,97 @@ class Database(rdbms_common.Database):
             's_last_use varchar, s_user varchar)')
         self.cursor.execute('create index sessions_key_idx on sessions(s_key)')
 
+    def add_actor_column(self):
+        # update existing tables to have the new actor column
+        tables = self.database_schema['tables']
+        for classname, spec in self.classes.items():
+            if tables.has_key(classname):
+                dbspec = tables[classname]
+                self.update_class(spec, dbspec, force=1, adding_actor=1)
+
+    def update_class(self, spec, old_spec, force=0, adding_actor=0):
+        ''' Determine the differences between the current spec and the
+            database version of the spec, and update where necessary.
+
+            If 'force' is true, update the database anyway.
+
+            SQLite doesn't have ALTER TABLE, so we have to copy and
+            regenerate the tables with the new schema.
+        '''
+        new_has = spec.properties.has_key
+        new_spec = spec.schema()
+        new_spec[1].sort()
+        old_spec[1].sort()
+        if not force and new_spec == old_spec:
+            # no changes
+            return 0
+
+        if __debug__:
+            print >>hyperdb.DEBUG, 'update_class FIRING'
+
+        # detect multilinks that have been removed, and drop their table
+        old_has = {}
+        for name,prop in old_spec[1]:
+            old_has[name] = 1
+            if new_has(name) or not isinstance(prop, hyperdb.Multilink):
+                continue
+            # it's a multilink, and it's been removed - drop the old
+            # table. First drop indexes.
+            self.drop_multilink_table_indexes(spec.classname, ml)
+            sql = 'drop table %s_%s'%(spec.classname, prop)
+            if __debug__:
+                print >>hyperdb.DEBUG, 'update_class', (self, sql)
+            self.cursor.execute(sql)
+        old_has = old_has.has_key
+
+        # now figure how we populate the new table
+        if adding_actor:
+            fetch = ['_activity', '_creation', '_creator']
+        else:
+            fetch = ['_actor', '_activity', '_creation', '_creator']
+        properties = spec.getprops()
+        for propname,x in new_spec[1]:
+            prop = properties[propname]
+            if isinstance(prop, hyperdb.Multilink):
+                if force or not old_has(propname):
+                    # we need to create the new table
+                    self.create_multilink_table(spec, propname)
+            elif old_has(propname):
+                # we copy this col over from the old table
+                fetch.append('_'+propname)
+
+        # select the data out of the old table
+        fetch.append('id')
+        fetch.append('__retired__')
+        fetchcols = ','.join(fetch)
+        cn = spec.classname
+        sql = 'select %s from _%s'%(fetchcols, cn)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'update_class', (self, sql)
+        self.cursor.execute(sql)
+        olddata = self.cursor.fetchall()
+
+        # TODO: update all the other index dropping code
+        self.drop_class_table_indexes(cn, old_spec[0])
+
+        # drop the old table
+        self.cursor.execute('drop table _%s'%cn)
+
+        # create the new table
+        self.create_class_table(spec)
+
+        if olddata:
+            # do the insert of the old data - the new columns will have
+            # NULL values
+            args = ','.join([self.arg for x in fetch])
+            sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
+            if __debug__:
+                print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
+            for entry in olddata:
+                self.cursor.execute(sql, tuple(entry))
+
+        return 1
+
     def sql_close(self):
         ''' Squash any error caused by us already having closed the
             connection.
index 40b0d248575c38308346394d4ca7bb9595012854..c2d341c5071e30f0065f8794cd3808fde39d0f9e 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: rdbms_common.py,v 1.78 2004-03-12 05:36:26 richard Exp $
+# $Id: rdbms_common.py,v 1.79 2004-03-15 05:50:20 richard Exp $
 ''' Relational database (SQL) backend common code.
 
 Basics:
@@ -188,6 +188,8 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             # version 1 doesn't have the OTK, session and indexing in the
             # database
             self.create_version_2_tables()
+            # version 1 also didn't have the actor column
+            self.add_actor_column()
         else:
             return 0
 
@@ -210,7 +212,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             "properties" is a list of (name, prop) where prop may be an
             instance of a hyperdb "type" _or_ a string repr of that type.
         '''
-        cols = ['_activity', '_creator', '_creation']
+        cols = ['_actor', '_activity', '_creator', '_creation']
         mls = []
         # add the multilinks separately
         for col, prop in properties:
@@ -240,62 +242,64 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if __debug__:
             print >>hyperdb.DEBUG, 'update_class FIRING'
 
+        # detect key prop change for potential index change
+        keyprop_changes = 0
+        if new_spec[0] != old_spec[0]:
+            keyprop_changes = {'remove': old_spec[0], 'add': new_spec[0]}
+
         # detect multilinks that have been removed, and drop their table
         old_has = {}
-        for name,prop in old_spec[1]:
+        for name, prop in old_spec[1]:
             old_has[name] = 1
-            if new_has(name) or not isinstance(prop, Multilink):
+            if new_has(name):
                 continue
-            # it's a multilink, and it's been removed - drop the old
-            # table. First drop indexes.
-            self.drop_multilink_table_indexes(spec.classname, ml)
-            sql = 'drop table %s_%s'%(spec.classname, prop)
+
+            if isinstance(prop, Multilink):
+                # first drop indexes.
+                self.drop_multilink_table_indexes(spec.classname, ml)
+
+                # now the multilink table itself
+                sql = 'drop table %s_%s'%(spec.classname, prop)
+            else:
+                # if this is the key prop, drop the index first
+                if old_spec[0] == prop:
+                    self.drop_class_table_key_index(spec.classname, prop)
+                    del keyprop_changes['remove']
+
+                # drop the column
+                sql = 'alter table _%s drop column _%s'%(spec.classname, prop)
+
             if __debug__:
                 print >>hyperdb.DEBUG, 'update_class', (self, sql)
             self.cursor.execute(sql)
         old_has = old_has.has_key
 
-        # now figure how we populate the new table
-        fetch = ['_activity', '_creation', '_creator']
-        properties = spec.getprops()
-        for propname,x in new_spec[1]:
-            prop = properties[propname]
-            if isinstance(prop, Multilink):
-                if force or not old_has(propname):
-                    # we need to create the new table
-                    self.create_multilink_table(spec, propname)
-            elif old_has(propname):
-                # we copy this col over from the old table
-                fetch.append('_'+propname)
-
-        # select the data out of the old table
-        fetch.append('id')
-        fetch.append('__retired__')
-        fetchcols = ','.join(fetch)
-        cn = spec.classname
-        sql = 'select %s from _%s'%(fetchcols, cn)
-        if __debug__:
-            print >>hyperdb.DEBUG, 'update_class', (self, sql)
-        self.cursor.execute(sql)
-        olddata = self.cursor.fetchall()
-
-        # TODO: update all the other index dropping code
-        self.drop_class_table_indexes(cn, old_spec[0])
+        # if we didn't remove the key prop just then, but the key prop has
+        # changed, we still need to remove the old index
+        if keyprop_changes.has_key('remove'):
+            self.drop_class_table_key_index(spec.classname,
+                keyprop_changes['remove'])
 
-        # drop the old table
-        self.cursor.execute('drop table _%s'%cn)
+        # add new columns
+        for propname, x in new_spec[1]:
+            if old_has(propname):
+                continue
+            sql = 'alter table _%s add column _%s varchar(255)'%(
+                spec.classname, propname)
+            if __debug__:
+                print >>hyperdb.DEBUG, 'update_class', (self, sql)
+            self.cursor.execute(sql)
 
-        # create the new table
-        self.create_class_table(spec)
+            # if the new column is a key prop, we need an index!
+            if new_spec[0] == propname:
+                self.create_class_table_key_index(spec.classname, propname)
+                del keyprop_changes['add']
 
-        if olddata:
-            # do the insert
-            args = ','.join([self.arg for x in fetch])
-            sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
-            if __debug__:
-                print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
-            for entry in olddata:
-                self.cursor.execute(sql, tuple(entry))
+        # if we didn't add the key prop just then, but the key prop has
+        # changed, we still need to add the new index
+        if keyprop_changes.has_key('add'):
+            self.create_class_table_key_index(spec.classname,
+                keyprop_changes['add'])
 
         return 1
 
@@ -352,10 +356,8 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # drop the old table indexes first
         l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
         if key:
-            # key prop too?
             l.append('_%s_%s_idx'%(cn, key))
 
-        # TODO: update all the other index dropping code
         table_name = '_%s'%cn
         for index_name in l:
             if not self.sql_index_exists(table_name, index_name):
@@ -365,6 +367,28 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
             self.cursor.execute(index_sql)
 
+    def create_class_table_key_index(self, cn, key):
+        ''' create the class table for the given spec
+        '''
+        if __debug__:
+            print >>hyperdb.DEBUG, 'update_class setting keyprop %r'% \
+                key
+        index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(cn, key,
+            cn, key)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'create_index', (self, index_sql3)
+        self.cursor.execute(index_sql3)
+
+    def drop_class_table_key_index(self, cn, key):
+        table_name = '_%s'%cn
+        index_name = '_%s_%s_idx'%(cn, key)
+        if not self.sql_index_exists(table_name, index_name):
+            return
+        sql = 'drop index '+index_name
+        if __debug__:
+            print >>hyperdb.DEBUG, 'drop_index', (self, sql)
+        self.cursor.execute(sql)
+
     def create_journal_table(self, spec):
         ''' create the journal table for a class given the spec and 
             already-determined cols
@@ -380,11 +404,11 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
 
     def create_journal_table_indexes(self, spec):
         # index on nodeid
-        index_sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
+        sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
                         spec.classname, spec.classname)
         if __debug__:
-            print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
-        self.cursor.execute(index_sql)
+            print >>hyperdb.DEBUG, 'create_index', (self, sql)
+        self.cursor.execute(sql)
 
     def drop_journal_table_indexes(self, classname):
         index_name = '%s_journ_idx'%classname
@@ -601,7 +625,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             # calling code's node assumptions)
             node = node.copy()
             node['creation'] = node['activity'] = date.Date()
-            node['creator'] = self.getuid()
+            node['actor'] = node['creator'] = self.getuid()
 
         # default the non-multilink columns
         for col, prop in cl.properties.items():
@@ -657,6 +681,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # add the special props
         values = values.copy()
         values['activity'] = date.Date()
+        values['actor'] = self.getuid()
 
         # make db-friendly
         values = self.serialise(classname, values)
@@ -1065,10 +1090,10 @@ class Class(hyperdb.Class):
         or a ValueError is raised.  The keyword arguments in 'properties'
         must map names to property objects, or a TypeError is raised.
         '''
-        if (properties.has_key('creation') or properties.has_key('activity')
-                or properties.has_key('creator')):
-            raise ValueError, '"creation", "activity" and "creator" are '\
-                'reserved'
+        for name in 'creation activity creator actor'.split():
+            if properties.has_key(name):
+                raise ValueError, '"creation", "activity", "creator" and '\
+                    '"actor" are reserved'
 
         self.classname = classname
         self.properties = properties
@@ -1132,8 +1157,10 @@ class Class(hyperdb.Class):
         if self.db.journaltag is None:
             raise DatabaseError, 'Database open read-only'
 
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
+        if propvalues.has_key('creator') or propvalues.has_key('actor') or \
+             propvalues.has_key('creation') or propvalues.has_key('activity'):
+            raise KeyError, '"creator", "actor", "creation" and '\
+                '"activity" are reserved'
 
         # new node's id
         newid = self.db.newid(self.classname)
@@ -1358,6 +1385,8 @@ class Class(hyperdb.Class):
             creation = None
         if d.has_key('activity'):
             del d['activity']
+        if d.has_key('actor'):
+            del d['actor']
         self.db.addjournal(self.classname, newid, 'create', {}, creator,
             creation)
         return newid
@@ -1393,6 +1422,11 @@ class Class(hyperdb.Class):
                 return d['creator']
             else:
                 return self.db.getuid()
+        if propname == 'actor':
+            if d.has_key('actor'):
+                return d['actor']
+            else:
+                return self.db.getuid()
 
         # get the property (raises KeyErorr if invalid)
         prop = self.properties[propname]
@@ -1433,8 +1467,10 @@ class Class(hyperdb.Class):
         if not propvalues:
             return propvalues
 
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
+        if propvalues.has_key('creation') or propvalues.has_key('creator') or \
+                propvalues.has_key('actor') or propvalues.has_key('activity'):
+            raise KeyError, '"creation", "creator", "actor" and '\
+                '"activity" are reserved'
 
         if propvalues.has_key('id'):
             raise KeyError, '"id" is reserved'
@@ -2175,6 +2211,7 @@ class Class(hyperdb.Class):
             d['creation'] = hyperdb.Date()
             d['activity'] = hyperdb.Date()
             d['creator'] = hyperdb.Link('user')
+            d['actor'] = hyperdb.Link('user')
         return d
 
     def addprop(self, **properties):
@@ -2345,7 +2382,8 @@ class IssueClass(Class, roundupdb.IssueClass):
         '''The newly-created class automatically includes the "messages",
         "files", "nosy", and "superseder" properties.  If the 'properties'
         dictionary attempts to specify any of these properties or a
-        "creation" or "activity" property, a ValueError is raised.
+        "creation", "creator", "activity" or "actor" property, a ValueError
+        is raised.
         '''
         if not properties.has_key('title'):
             properties['title'] = hyperdb.String(indexme='yes')
index 0a39ee3116f12d9cbf418ca7c45c3dbfdc1fc995..f286dbd2181ef9331098fe6c77d8cc6bd9f3329d 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundupdb.py,v 1.100 2004-03-05 00:08:09 richard Exp $
+# $Id: roundupdb.py,v 1.101 2004-03-15 05:50:19 richard Exp $
 
 """Extending hyperdb with types specific to issue-tracking.
 """
@@ -408,7 +408,7 @@ class IssueClass:
         for key in oldvalues.keys():
             if key in ['files','messages']:
                 continue
-            if key in ('activity', 'creator', 'creation'):
+            if key in ('actor', 'activity', 'creator', 'creation'):
                 continue
             # not all keys from oldvalues might be available in database
             # this happens when property was deleted
index 9ed40405861e8c233335ca9668328f1d1aa7f1a2..0a7093bfb0983210581615a69f1e2deb58964da4 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: dbinit.py,v 1.4 2004-03-12 05:36:26 richard Exp $
+# $Id: dbinit.py,v 1.5 2004-03-15 05:50:20 richard Exp $
 
 import os
 
@@ -107,6 +107,7 @@ def open(name=None):
         db.security.addPermissionToRole('User', p)
     for cl in 'priority', 'status':
         p = db.security.getPermission('View', cl)
+        db.security.addPermissionToRole('User', p)
 
     # and give the regular users access to the web and email interface
     p = db.security.getPermission('Web Access')
index dd934852d79725fafc0507d1f47bba4e837d4f39..8deb1c18c8bb9f41557fd8b370b789c795613535 100644 (file)
@@ -17,10 +17,11 @@ You are not allowed to view this page.
    <th tal:condition="request/show/id">ID</th>
    <th tal:condition="request/show/creation">Creation</th>
    <th tal:condition="request/show/activity">Activity</th>
+   <th tal:condition="request/show/actor">Actor</th>
    <th tal:condition="request/show/topic">Topic</th>
    <th tal:condition="request/show/title">Title</th>
    <th tal:condition="request/show/status">Status</th>
-   <th tal:condition="request/show/creator">Created&nbsp;By</th>
+   <th tal:condition="request/show/creator">Creator</th>
    <th tal:condition="request/show/assignedto">Assigned&nbsp;To</th>
   </tr>
  <tal:block tal:repeat="i batch">
@@ -39,6 +40,8 @@ You are not allowed to view this page.
        tal:content="i/creation/reldate">&nbsp;</td>
    <td class="date" tal:condition="request/show/activity"
        tal:content="i/activity/reldate">&nbsp;</td>
+   <td class="date" tal:condition="request/show/actor"
+       tal:content="python:i.actor.plain() or default">&nbsp;</td>
    <td tal:condition="request/show/topic"
        tal:content="python:i.topic.plain() or default">&nbsp;</td>
    <td tal:condition="request/show/title">
index 7423a8d908c691661a42e03db9fc5a6fc5bff5a9..cc83601075ceb9393f874d326de244bee2877dfb 100644 (file)
@@ -95,7 +95,7 @@ python:db.user.classhelp('username,realname,address', property='nosy', width='60
 
 <p tal:condition="context/id" tal:content="structure string:Created on
   <b>${context/creation}</b> by <b>${context/creator}</b>, last
-  changed <b>${context/activity}</b>.">activity info
+  changed <b>${context/activity}</b> by <b>${context/actor}</b>.">activity info
 </p>
 
 <table class="files" tal:condition="context/files">
index 102d721d7e9f67ce8b9178ae26bd8592c8842b14..69ec8b6a4ff43de2163bf4c0d07bf060a714df27 100644 (file)
   <td>&nbsp;</td>
 </tr>
 
+<tr tal:define="name string:actor">
+  <th>Actor:</th>
+  <td metal:use-macro="search_select">
+    <option metal:fill-slot="extra_options"
+            tal:attributes="value request/user/id">done by me</option>
+  </td>
+  <td metal:use-macro="column_input"></td>
+  <td metal:use-macro="sort_input"></td>
+  <td>&nbsp;</td>
+</tr>
+
 <tr tal:define="name string:priority;
                 db_klass string:priority;
                 db_content string:name;">
index c7a52d25b4d91358c6285127938ded2aa42b461a..b2ffcd1e4656aff7c50f8df65f6456d98049506a 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: db_test_base.py,v 1.15 2004-03-12 04:09:00 richard Exp $ 
+# $Id: db_test_base.py,v 1.16 2004-03-15 05:50:20 richard Exp $ 
 
 import unittest, os, shutil, errno, imp, sys, time, pprint
 
@@ -43,6 +43,8 @@ def setupSchema(db, create, module):
     if create:
         user.create(username="admin", roles='Admin',
             password=password.Password('sekrit'))
+        user.create(username="fred", roles='User',
+            password=password.Password('sekrit'))
         status.create(name="unread")
         status.create(name="in-progress")
         status.create(name="testing")
@@ -83,6 +85,30 @@ class DBTest(MyTestCase):
     def testRefresh(self):
         self.db.refresh_database()
 
+    #
+    # automatic properties (well, the two easy ones anyway)
+    #
+    def testCreatorProperty(self):
+        id1 = self.db.issue.create()
+        self.db.commit()
+        self.db.close()
+        self.db = self.module.Database(config, 'fred')
+        setupSchema(self.db, 0, self.module)
+        i = self.db.issue
+        id2 = i.create()
+        self.assertNotEqual(id1, id2)
+        self.assertNotEqual(i.get(id1, 'creator'), i.get(id2, 'creator'))
+
+    def testActorProperty(self):
+        id1 = self.db.issue.create()
+        self.db.commit()
+        self.db.close()
+        self.db = self.module.Database(config, 'fred')
+        setupSchema(self.db, 0, self.module)
+        i = self.db.issue
+        i.set(id1, title='asfasd')
+        self.assertNotEqual(i.get(id1, 'creator'), i.get(id1, 'actor'))
+
     #
     # basic operations
     #
@@ -417,6 +443,7 @@ class DBTest(MyTestCase):
         ar(KeyError, self.db.status.create, creation=1)
         ar(KeyError, self.db.status.create, creator=1)
         ar(KeyError, self.db.status.create, activity=1)
+        ar(KeyError, self.db.status.create, actor=1)
         # invalid property name
         ar(KeyError, self.db.status.create, foo='foo')
         # key name clash
@@ -891,7 +918,7 @@ class DBTest(MyTestCase):
         props = self.db.issue.getprops()
         keys = props.keys()
         keys.sort()
-        self.assertEqual(keys, ['activity', 'assignedto', 'creation',
+        self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
             'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
             'nosy', 'status', 'superseder', 'title'])
         self.assertEqual(self.db.issue.get('1', "fixer"), None)
@@ -905,7 +932,7 @@ class DBTest(MyTestCase):
         props = self.db.issue.getprops()
         keys = props.keys()
         keys.sort()
-        self.assertEqual(keys, ['activity', 'assignedto', 'creation',
+        self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
             'creator', 'deadline', 'files', 'foo', 'id', 'messages',
             'nosy', 'status', 'superseder'])
         self.assertEqual(self.db.issue.list(), ['1'])
@@ -920,7 +947,7 @@ class DBTest(MyTestCase):
         props = self.db.issue.getprops()
         keys = props.keys()
         keys.sort()
-        self.assertEqual(keys, ['activity', 'assignedto', 'creation',
+        self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
             'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
             'nosy', 'status', 'superseder'])
         self.assertEqual(self.db.issue.list(), ['1'])
@@ -963,6 +990,8 @@ class SchemaTest(MyTestCase):
             activity=String())
         self.assertRaises(ValueError, self.module.Class, self.db, "a",
             creator=String())
+        self.assertRaises(ValueError, self.module.Class, self.db, "a",
+            actor=String())
 
     def init_a(self):
         self.db = self.module.Database(config, 'admin')