Code

Finished implementation of session and one-time-key stores for RDBMS
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Thu, 18 Mar 2004 01:58:46 +0000 (01:58 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Thu, 18 Mar 2004 01:58:46 +0000 (01:58 +0000)
backends. Refactored the API of sessions and their interaction with the
backend database a fair bit too.

Added some session tests. Nothing testing ageing yet, 'cos that's a pain
inna ass to test :)

Note: metakit backend still uses the *dbm implementation. It might
want to implement its own session store some day, as it'll be faster than
the *dbm one.

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

22 files changed:
CHANGES.txt
roundup/backends/back_anydbm.py
roundup/backends/back_metakit.py
roundup/backends/back_mysql.py
roundup/backends/back_postgresql.py
roundup/backends/back_sqlite.py
roundup/backends/rdbms_common.py
roundup/backends/sessions.py [deleted file]
roundup/backends/sessions_dbm.py [new file with mode: 0644]
roundup/backends/sessions_rdbms.py [new file with mode: 0644]
roundup/cgi/actions.py
roundup/cgi/client.py
templates/classic/html/style.css
test/db_test_base.py
test/session_common.py [new file with mode: 0644]
test/test_anydbm.py
test/test_bsddb.py
test/test_bsddb3.py
test/test_metakit.py
test/test_mysql.py
test/test_postgresql.py
test/test_sqlite.py

index 710b1046440ff7321adde6fc19d28f01a933969a..f0c79f6082e056cdfb7035a830f73f03ceabb0d3 100644 (file)
@@ -13,6 +13,7 @@ Feature:
 - added postgresql backend (originally from sf patch 761740, many changes
   since)
 - all RDBMS backends now have indexes on several columns
+- RDBMS backends implement their session and one-time-key stores
 - change nosymessage and send_message to accept msgid=None (RFE #707235).
 - handle Resent-From: headers (sf bug 841151)
 - always sort MultilinkHTMLProperty in the correct order, usually
index 64c6236aa3cb8ea66e40b5c8242bcfd48baf9750..7091d19bd218995e1bd78a529f6c06b49bb207eb 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.137 2004-03-15 05:50:20 richard Exp $
+#$Id: back_anydbm.py,v 1.138 2004-03-18 01:58:45 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
@@ -36,7 +36,7 @@ except AssertionError:
 import whichdb, os, marshal, re, weakref, string, copy
 from roundup import hyperdb, date, password, roundupdb, security
 from blobfiles import FileStorage
-from sessions import Sessions, OneTimeKeys
+from sessions_dbm import Sessions, OneTimeKeys
 from roundup.indexer import Indexer
 from roundup.backends import locking
 from roundup.hyperdb import String, Password, Date, Interval, Link, \
@@ -79,8 +79,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.destroyednodes = {}# keep track of the destroyed nodes by class
         self.transactions = []
         self.indexer = Indexer(self.dir)
-        self.sessions = Sessions(self.config)
-        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
         # ensure files are group readable and writable
         os.umask(0002)
@@ -103,6 +101,12 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         """
         self.reindex()
 
+    def getSessionManager(self):
+        return Sessions(self)
+
+    def getOTKManager(self):
+        return OneTimeKeys(self)
+
     def reindex(self):
         for klass in self.classes.values():
             for nodeid in klass.list():
index 055782ea49c4fcbd66095d1ead40ab5765347c0c..cf3f59f99c594947f43e5771f2d5deb94f7c2093 100755 (executable)
@@ -1,4 +1,4 @@
-# $Id: back_metakit.py,v 1.61 2004-03-12 05:36:26 richard Exp $
+# $Id: back_metakit.py,v 1.62 2004-03-18 01:58:45 richard Exp $
 '''Metakit backend for Roundup, originally by Gordon McMillan.
 
 Known Current Bugs:
@@ -43,7 +43,7 @@ BACKWARDS_COMPATIBLE = True
 
 from roundup import hyperdb, date, password, roundupdb, security
 import metakit
-from sessions import Sessions, OneTimeKeys
+from sessions_dbm import Sessions, OneTimeKeys
 import re, marshal, os, sys, time, calendar
 from roundup import indexer
 import locking
@@ -81,8 +81,6 @@ class _Database(hyperdb.Database, roundupdb.Database):
         self.lockfile = None
         self._db = self.__open()
         self.indexer = Indexer(self.config.DATABASE, self._db)
-        self.sessions = Sessions(self.config)
-        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
 
         os.umask(0002)
@@ -101,6 +99,12 @@ class _Database(hyperdb.Database, roundupdb.Database):
                 klass.index(nodeid)
         self.indexer.save_index()
 
+    def getSessionManager(self):
+        return Sessions(self)
+
+    def getOTKManager(self):
+        return OneTimeKeys(self)
+
     # --- defined in ping's spec
     def __getattr__(self, classname):
         if classname == 'transactions':
index a02b445ec99b427811742f8c40e877fbe0f6ad22..3afd3d36a16847ba89f5f26756837086352a2820 100644 (file)
@@ -86,22 +86,25 @@ class Database(Database):
     # use BDB to pass all unit tests.
     mysql_backend = 'InnoDB'
     #mysql_backend = 'BDB'
-    
-    def sql_open_connection(self):
-        # make sure the database actually exists
-        if not db_exists(self.config):
-            db_create(self.config)
 
+    def sql_open_connection(self):
         db = getattr(self.config, 'MYSQL_DATABASE')
         try:
-            self.conn = MySQLdb.connect(*db)
+            conn = MySQLdb.connect(*db)
         except MySQLdb.OperationalError, message:
             raise DatabaseError, message
+        cursor = conn.cursor()
+        cursor.execute("SET AUTOCOMMIT=0")
+        cursor.execute("BEGIN")
+        return (conn, cursor)
+    
+    def open_connection(self):
+        # make sure the database actually exists
+        if not db_exists(self.config):
+            db_create(self.config)
+
+        self.conn, self.cursor = self.sql_open_connection()
 
-        self.cursor = self.conn.cursor()
-        # start transaction
-        self.sql("SET AUTOCOMMIT=0")
-        self.sql("BEGIN")
         try:
             self.load_dbschema()
         except MySQLdb.OperationalError, message:
@@ -124,9 +127,10 @@ class Database(Database):
         self.cursor.execute('CREATE TABLE otks (otk_key VARCHAR(255), '
             'otk_value VARCHAR(255), otk_time FLOAT(20))')
         self.cursor.execute('CREATE INDEX otks_key_idx ON otks(otk_key)')
-        self.cursor.execute('CREATE TABLE sessions (s_key VARCHAR(255), '
-            's_last_use FLOAT(20), s_user VARCHAR(255))')
-        self.cursor.execute('CREATE INDEX sessions_key_idx ON sessions(s_key)')
+        self.cursor.execute('CREATE TABLE sessions (session_key VARCHAR(255), '
+            'session_time FLOAT(20), session_value VARCHAR(255))')
+        self.cursor.execute('CREATE INDEX sessions_key_idx ON '
+            'sessions(session_key)')
 
     def add_actor_column(self):
         # update existing tables to have the new actor column
index fa9ba09e9de69ecda1807bd56ae4450f7a2c3d79..a09086b50db3bdb992cc9e91876d338eba21d39b 100644 (file)
@@ -83,19 +83,24 @@ class Database(rdbms_common.Database):
     arg = '%s'
 
     def sql_open_connection(self):
+        db = getattr(self.config, 'POSTGRESQL_DATABASE')
+        try:
+            conn = psycopg.connect(**db)
+        except psycopg.OperationalError, message:
+            raise hyperdb.DatabaseError, message
+
+        cursor = conn.cursor()
+
+        return (conn, cursor)
+
+    def open_connection(self):
         if not db_exists(self.config):
             db_create(self.config)
 
         if __debug__:
             print >>hyperdb.DEBUG, '+++ open database connection +++'
 
-        db = getattr(self.config, 'POSTGRESQL_DATABASE')
-        try:
-            self.conn = psycopg.connect(**db)
-        except psycopg.OperationalError, message:
-            raise hyperdb.DatabaseError, message
-
-        self.cursor = self.conn.cursor()
+        self.conn, self.cursor = self.sql_open_connection()
 
         try:
             self.load_dbschema()
@@ -111,9 +116,10 @@ class Database(rdbms_common.Database):
         self.cursor.execute('CREATE TABLE otks (otk_key VARCHAR(255), '
             'otk_value VARCHAR(255), otk_time FLOAT(20))')
         self.cursor.execute('CREATE INDEX otks_key_idx ON otks(otk_key)')
-        self.cursor.execute('CREATE TABLE sessions (s_key VARCHAR(255), '
-            's_last_use FLOAT(20), s_user VARCHAR(255))')
-        self.cursor.execute('CREATE INDEX sessions_key_idx ON sessions(s_key)')
+        self.cursor.execute('CREATE TABLE sessions (session_key VARCHAR(255), '
+            'session_time FLOAT(20), session_value VARCHAR(255))')
+        self.cursor.execute('CREATE INDEX sessions_key_idx ON '
+            'sessions(session_key)')
 
     def add_actor_column(self):
         # update existing tables to have the new actor column
index 04100d09316daac2229e5979670148f0cbdc4b30..e1105d3488484e85709872189091dbdd7ac09dac 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: back_sqlite.py,v 1.16 2004-03-15 05:50:20 richard Exp $
+# $Id: back_sqlite.py,v 1.17 2004-03-18 01:58:45 richard Exp $
 '''Implements a backend for SQLite.
 
 See https://pysqlite.sourceforge.net/ for pysqlite info
@@ -17,18 +17,24 @@ class Database(rdbms_common.Database):
     arg = '%s'
 
     def sql_open_connection(self):
+        db = os.path.join(self.config.DATABASE, 'db')
+        conn = sqlite.connect(db=db)
+        cursor = conn.cursor()
+        return (conn, cursor)
+
+    def open_connection(self):
         # ensure files are group readable and writable
         os.umask(0002)
-        db = os.path.join(self.config.DATABASE, 'db')
 
-        # lock it
+        # lock the database
+        db = os.path.join(self.config.DATABASE, 'db')
         lockfilenm = db[:-3] + 'lck'
         self.lockfile = locking.acquire_lock(lockfilenm)
         self.lockfile.write(str(os.getpid()))
         self.lockfile.flush()
 
-        self.conn = sqlite.connect(db=db)
-        self.cursor = self.conn.cursor()
+        (self.conn, self.cursor) = self.sql_open_connection()
+
         try:
             self.load_dbschema()
         except sqlite.DatabaseError, error:
@@ -40,13 +46,24 @@ class Database(rdbms_common.Database):
             self.cursor.execute('create index ids_name_idx on ids(name)')
             self.create_version_2_tables()
 
+    def close(self):
+        ''' Close off the connection.
+        '''
+        self.sql_close()
+        if self.lockfile is not None:
+            locking.release_lock(self.lockfile)
+        if self.lockfile is not None:
+            self.lockfile.close()
+            self.lockfile = None
+
     def create_version_2_tables(self):
         self.cursor.execute('create table otks (otk_key varchar, '
-            'otk_value varchar, otk_time varchar)')
+            'otk_value varchar, otk_time integer)')
         self.cursor.execute('create index otks_key_idx on otks(otk_key)')
-        self.cursor.execute('create table sessions (s_key varchar, '
-            's_last_use varchar, s_user varchar)')
-        self.cursor.execute('create index sessions_key_idx on sessions(s_key)')
+        self.cursor.execute('create table sessions (session_key varchar, '
+            'session_time integer, session_value varchar)')
+        self.cursor.execute('create index sessions_key_idx on '
+                'sessions(session_key)')
 
     def add_actor_column(self):
         # update existing tables to have the new actor column
index 2d030365750679496c7a8925c1e5468814cbfc81..2894cc63e35a8374b17ed070bd0fd0f6955353a9 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: rdbms_common.py,v 1.80 2004-03-17 22:01:37 richard Exp $
+# $Id: rdbms_common.py,v 1.81 2004-03-18 01:58:45 richard Exp $
 ''' Relational database (SQL) backend common code.
 
 Basics:
@@ -40,7 +40,7 @@ from roundup.backends import locking
 # support
 from blobfiles import FileStorage
 from roundup.indexer import Indexer
-from sessions import Sessions, OneTimeKeys
+from sessions_rdbms import Sessions, OneTimeKeys
 from roundup.date import Range
 
 # number of rows to keep in memory
@@ -60,8 +60,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.dir = config.DATABASE
         self.classes = {}
         self.indexer = Indexer(self.dir)
-        self.sessions = Sessions(self.config)
-        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
 
         # additional transaction support for external files and the like
@@ -76,13 +74,19 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.lockfile = None
 
         # open a connection to the database, creating the "conn" attribute
-        self.sql_open_connection()
+        self.open_connection()
 
     def clearCache(self):
         self.cache = {}
         self.cache_lru = []
 
-    def sql_open_connection(self):
+    def getSessionManager(self):
+        return Sessions(self)
+
+    def getOTKManager(self):
+        return OneTimeKeys(self)
+
+    def open_connection(self):
         ''' Open a connection to the database, creating it if necessary.
 
             Must call self.load_dbschema()
@@ -1069,11 +1073,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         ''' Close off the connection.
         '''
         self.sql_close()
-        if self.lockfile is not None:
-            locking.release_lock(self.lockfile)
-        if self.lockfile is not None:
-            self.lockfile.close()
-            self.lockfile = None
 
 #
 # The base Class class
diff --git a/roundup/backends/sessions.py b/roundup/backends/sessions.py
deleted file mode 100644 (file)
index 5f9a79b..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-#$Id: sessions.py,v 1.10 2004-02-26 04:20:45 drkorg Exp $
-"""This module defines a very basic store that's used by the CGI interface
-to store session and one-time-key information.
-
-Yes, it's called "sessions" - because originally it only defined a session
-class. It's now also used for One Time Key handling too.
-"""
-__docformat__ = 'restructuredtext'
-
-import anydbm, whichdb, os, marshal, time
-
-class BasicDatabase:
-    ''' Provide a nice encapsulation of an anydbm store.
-
-        Keys are id strings, values are automatically marshalled data.
-    '''
-    _db_type = None
-
-    def __init__(self, config):
-        self.config = config
-        self.dir = config.DATABASE
-        # ensure files are group readable and writable
-        os.umask(0002)
-
-    def clear(self):
-        path = os.path.join(self.dir, self.name)
-        if os.path.exists(path):
-            os.remove(path)
-        elif os.path.exists(path+'.db'):    # dbm appends .db
-            os.remove(path+'.db')
-
-    def cache_db_type(self, path):
-        ''' determine which DB wrote the class file, and cache it as an
-            attribute of __class__ (to allow for subclassed DBs to be
-            different sorts)
-        '''
-        db_type = ''
-        if os.path.exists(path):
-            db_type = whichdb.whichdb(path)
-            if not db_type:
-                raise hyperdb.DatabaseError, "Couldn't identify database type"
-        elif os.path.exists(path+'.db'):
-            # if the path ends in '.db', it's a dbm database, whether
-            # anydbm says it's dbhash or not!
-            db_type = 'dbm'
-        self.__class__._db_type = db_type
-
-    def get(self, infoid, value):
-        db = self.opendb('c')
-        try:
-            if db.has_key(infoid):
-                values = marshal.loads(db[infoid])
-            else:
-                return None
-            return values.get(value, None)
-        finally:
-            db.close()
-
-    def getall(self, infoid):
-        db = self.opendb('c')
-        try:
-            try:
-                return marshal.loads(db[infoid])
-            except KeyError:
-                raise KeyError, 'No such One Time Key "%s"'%infoid
-        finally:
-            db.close()
-
-    def set(self, infoid, **newvalues):
-        db = self.opendb('c')
-        try:
-            if db.has_key(infoid):
-                values = marshal.loads(db[infoid])
-            else:
-                values = {}
-            values.update(newvalues)
-            db[infoid] = marshal.dumps(values)
-        finally:
-            db.close()
-
-    def list(self):
-        db = self.opendb('r')
-        try:
-            return db.keys()
-        finally:
-            db.close()
-
-    def destroy(self, infoid):
-        db = self.opendb('c')
-        try:
-            if db.has_key(infoid):
-                del db[infoid]
-        finally:
-            db.close()
-
-    def opendb(self, mode):
-        '''Low-level database opener that gets around anydbm/dbm
-           eccentricities.
-        '''
-        # figure the class db type
-        path = os.path.join(os.getcwd(), self.dir, self.name)
-        if self._db_type is None:
-            self.cache_db_type(path)
-
-        db_type = self._db_type
-
-        # new database? let anydbm pick the best dbm
-        if not db_type:
-            return anydbm.open(path, 'c')
-
-        # open the database with the correct module
-        dbm = __import__(db_type)
-        return dbm.open(path, mode)
-
-    def commit(self):
-        pass
-
-    def updateTimestamp(self, sessid):
-        self.set(sessid, **{self.timestamp: time.time()})
-
-    def clean(self, now):
-        """Age sessions, remove when they haven't been used for a week.
-        """
-        week = 60*60*24*7
-        for sessid in self.list():
-            interval = now - self.get(sessid, self.timestamp)
-            if interval > week:
-                self.destroy(sessid)
-
-class Sessions(BasicDatabase):
-    name = 'sessions'
-    timestamp = 'last_use'
-
-class OneTimeKeys(BasicDatabase):
-    name = 'otks'
-    timestamp = '__time'
-
diff --git a/roundup/backends/sessions_dbm.py b/roundup/backends/sessions_dbm.py
new file mode 100644 (file)
index 0000000..f8f0222
--- /dev/null
@@ -0,0 +1,141 @@
+#$Id: sessions_dbm.py,v 1.1 2004-03-18 01:58:45 richard Exp $
+"""This module defines a very basic store that's used by the CGI interface
+to store session and one-time-key information.
+
+Yes, it's called "sessions" - because originally it only defined a session
+class. It's now also used for One Time Key handling too.
+"""
+__docformat__ = 'restructuredtext'
+
+import anydbm, whichdb, os, marshal, time
+
+class BasicDatabase:
+    ''' Provide a nice encapsulation of an anydbm store.
+
+        Keys are id strings, values are automatically marshalled data.
+    '''
+    _db_type = None
+
+    def __init__(self, db):
+        self.config = db.config
+        self.dir = db.config.DATABASE
+        # ensure files are group readable and writable
+        os.umask(0002)
+
+    def clear(self):
+        path = os.path.join(self.dir, self.name)
+        if os.path.exists(path):
+            os.remove(path)
+        elif os.path.exists(path+'.db'):    # dbm appends .db
+            os.remove(path+'.db')
+
+    def cache_db_type(self, path):
+        ''' determine which DB wrote the class file, and cache it as an
+            attribute of __class__ (to allow for subclassed DBs to be
+            different sorts)
+        '''
+        db_type = ''
+        if os.path.exists(path):
+            db_type = whichdb.whichdb(path)
+            if not db_type:
+                raise hyperdb.DatabaseError, "Couldn't identify database type"
+        elif os.path.exists(path+'.db'):
+            # if the path ends in '.db', it's a dbm database, whether
+            # anydbm says it's dbhash or not!
+            db_type = 'dbm'
+        self.__class__._db_type = db_type
+
+    _marker = []
+    def get(self, infoid, value, default=_marker):
+        db = self.opendb('c')
+        try:
+            if db.has_key(infoid):
+                values = marshal.loads(db[infoid])
+            else:
+                if default != self._marker:
+                    return default
+                raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+            return values.get(value, None)
+        finally:
+            db.close()
+
+    def getall(self, infoid):
+        db = self.opendb('c')
+        try:
+            try:
+                return marshal.loads(db[infoid])
+            except KeyError:
+                raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+        finally:
+            db.close()
+
+    def set(self, infoid, **newvalues):
+        db = self.opendb('c')
+        try:
+            if db.has_key(infoid):
+                values = marshal.loads(db[infoid])
+            else:
+                values = {'__timestamp': time.time()}
+            values.update(newvalues)
+            db[infoid] = marshal.dumps(values)
+        finally:
+            db.close()
+
+    def list(self):
+        db = self.opendb('r')
+        try:
+            return db.keys()
+        finally:
+            db.close()
+
+    def destroy(self, infoid):
+        db = self.opendb('c')
+        try:
+            if db.has_key(infoid):
+                del db[infoid]
+        finally:
+            db.close()
+
+    def opendb(self, mode):
+        '''Low-level database opener that gets around anydbm/dbm
+           eccentricities.
+        '''
+        # figure the class db type
+        path = os.path.join(os.getcwd(), self.dir, self.name)
+        if self._db_type is None:
+            self.cache_db_type(path)
+
+        db_type = self._db_type
+
+        # new database? let anydbm pick the best dbm
+        if not db_type:
+            return anydbm.open(path, 'c')
+
+        # open the database with the correct module
+        dbm = __import__(db_type)
+        return dbm.open(path, mode)
+
+    def commit(self):
+        pass
+
+    def close(self):
+        pass
+
+    def updateTimestamp(self, sessid):
+        self.set(sessid, __timestamp=time.time())
+
+    def clean(self, now):
+        """Age sessions, remove when they haven't been used for a week.
+        """
+        week = 60*60*24*7
+        for sessid in self.list():
+            interval = now - self.get(sessid, '__timestamp')
+            if interval > week:
+                self.destroy(sessid)
+
+class Sessions(BasicDatabase):
+    name = 'sessions'
+
+class OneTimeKeys(BasicDatabase):
+    name = 'otks'
+
diff --git a/roundup/backends/sessions_rdbms.py b/roundup/backends/sessions_rdbms.py
new file mode 100644 (file)
index 0000000..7ee3bc1
--- /dev/null
@@ -0,0 +1,90 @@
+#$Id: sessions_rdbms.py,v 1.1 2004-03-18 01:58:45 richard Exp $
+"""This module defines a very basic store that's used by the CGI interface
+to store session and one-time-key information.
+
+Yes, it's called "sessions" - because originally it only defined a session
+class. It's now also used for One Time Key handling too.
+"""
+__docformat__ = 'restructuredtext'
+
+import os, time
+
+class BasicDatabase:
+    ''' Provide a nice encapsulation of an RDBMS table.
+
+        Keys are id strings, values are automatically marshalled data.
+    '''
+    def __init__(self, db):
+        self.db = db
+        self.cursor = self.db.cursor
+
+    def clear(self):
+        self.cursor.execute('delete from %ss'%self.name)
+
+    _marker = []
+    def get(self, infoid, value, default=_marker):
+        n = self.name
+        self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
+            n, n, self.db.arg), (infoid,))
+        res = self.cursor.fetchone()
+        if not res:
+            if default != self._marker:
+                return default
+            raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+        values = eval(res[0])
+        return values.get(value, None)
+
+    def getall(self, infoid):
+        n = self.name
+        self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
+            n, n, self.db.arg), (infoid,))
+        res = self.cursor.fetchone()
+        if not res:
+            raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+        return eval(res[0])
+
+    def set(self, infoid, **newvalues):
+        c = self.cursor
+        n = self.name
+        a = self.db.arg
+        c.execute('select %s_value from %ss where %s_key=%s'%(n, n, n, a),
+            (infoid,))
+        res = c.fetchone()
+        if res:
+            values = eval(res[0])
+        else:
+            values = {}
+        values.update(newvalues)
+
+        if res:
+            sql = 'update %ss set %s_value=%s where %s_key=%s'%(n, n,
+                a, n, a)
+            args = (repr(values), infoid)
+        else:
+            sql = 'insert into %ss (%s_key, %s_time, %s_value) '\
+                'values (%s, %s, %s)'%(n, n, n, n, a, a, a)
+            args = (infoid, time.time(), repr(values))
+        c.execute(sql, args)
+
+    def destroy(self, infoid):
+        self.cursor.execute('delete from %ss where %s_key=%s'%(self.name,
+            self.name, self.db.arg), (infoid,))
+
+    def updateTimestamp(self, infoid):
+        self.cursor.execute('update %ss set %s_time=%s where %s_key=%s'%(
+            self.name, self.name, self.db.arg, self.name, self.db.arg),
+            (time.time(), infoid))
+
+    def clean(self, now):
+        """Age sessions, remove when they haven't been used for a week.
+        """
+        old = now - 60*60*24*7
+        self.cursor.execute('delete from %ss where %s_time < %s'%(self.name,
+            self.name, self.db.arg), (old, ))
+
+class Sessions(BasicDatabase):
+    name = 'session'
+
+class OneTimeKeys(BasicDatabase):
+    name = 'otk'
+
index 46ff0e8b81d6689158d0b6098497e7994ffecbcc..3859f968b30eb29850dd10d7a12cebf51995a172 100755 (executable)
@@ -533,7 +533,8 @@ class PassResetAction(Action):
         if self.form.has_key('otk'):
             # pull the rego information out of the otk database
             otk = self.form['otk'].value
-            uid = self.db.otks.get(otk, 'uid')
+            otks = self.db.getOTKManager()
+            uid = otks.get(otk, 'uid')
             if uid is None:
                 self.client.error_message.append("""Invalid One Time Key!
 (a Mozilla bug may cause this message to show up erroneously,
@@ -549,12 +550,12 @@ class PassResetAction(Action):
             newpw = password.generatePassword()
 
             cl = self.db.user
-# XXX we need to make the "default" page be able to display errors!
+            # XXX we need to make the "default" page be able to display errors!
             try:
                 # set the password
                 cl.set(uid, password=password.Password(newpw))
                 # clear the props from the otk database
-                self.db.otks.destroy(otk)
+                otks.destroy(otk)
                 self.db.commit()
             except (ValueError, KeyError), message:
                 self.client.error_message.append(str(message))
@@ -575,8 +576,8 @@ Your password is now: %(password)s
             if not self.client.standard_message([address], subject, body):
                 return
 
-            self.client.ok_message.append('Password reset and email sent to %s' %
-                                          address)
+            self.client.ok_message.append(
+                    'Password reset and email sent to %s'%address)
             return
 
         # no OTK, so now figure the user
@@ -602,8 +603,10 @@ Your password is now: %(password)s
 
         # generate the one-time-key and store the props for later
         otk = ''.join([random.choice(chars) for x in range(32)])
-        d = {'uid': uid, self.db.otks.timestamp: time.time()}
-        self.db.otks.set(otk, **d)
+        while otks.exists(otk):
+            otk = ''.join([random.choice(chars) for x in range(32)])
+        otks.set(otk, uid=uid)
+        self.db.commit()
 
         # send the email
         tracker_name = self.db.config.TRACKER_NAME
@@ -685,7 +688,6 @@ class RegisterAction(Action):
             pass
 
         # generate the one-time-key and store the props for later
-        otk = ''.join([random.choice(chars) for x in range(32)])
         for propname, proptype in self.db.user.getprops().items():
             value = props.get(propname, None)
             if value is None:
@@ -696,8 +698,10 @@ class RegisterAction(Action):
                 props[propname] = str(value)
             elif isinstance(proptype, hyperdb.Password):
                 props[propname] = str(value)
-        props[self.db.otks.timestamp] = time.time()
-        self.db.otks.set(otk, **props)
+        otks = self.db.getOTKManager()
+        while otks.exists(otk):
+            otk = ''.join([random.choice(chars) for x in range(32)])
+        otks.set(otk, **props)
 
         # send the email
         tracker_name = self.db.config.TRACKER_NAME
index d96aa168258a13532244194fd88ef6eabc16a394..03cfa642eab575652f69200029b2a043a61ddd21 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.165 2004-02-25 23:27:54 richard Exp $
+# $Id: client.py,v 1.166 2004-03-18 01:58:46 richard Exp $
 
 """WWW request handler (also used in the stand-alone server).
 """
@@ -250,8 +250,8 @@ class Client:
 
         Note: also cleans One Time Keys, and other "session" based stuff.
         """
-        sessions = self.db.sessions
-        last_clean = self.db.sessions.get('last_clean', 'last_use') or 0
+        sessions = self.db.getSessionManager()
+        last_clean = sessions.get('last_clean', 'last_use', 0)
 
         # time to clean?
         week = 60*60*24*7
@@ -260,9 +260,10 @@ class Client:
         if now - last_clean < hour:
             return
 
-        self.db.sessions.clean(now)
-        self.db.otks.clean(now)
-        self.db.sessions.set('last_clean', last_use=time.time())
+        sessions.clean(now)
+        self.db.getOTKManager().clean(now)
+        sessions.set('last_clean', last_use=time.time())
+        self.db.commit()
 
     def determine_user(self):
         ''' Determine who the user is
@@ -272,7 +273,7 @@ class Client:
 
         # make sure we have the session Class
         self.clean_sessions()
-        sessions = self.db.sessions
+        sessions = self.db.getSessionManager()
 
         # first up, try the REMOTE_USER var (from HTTP Basic Auth handled
         # by a front-end HTTP server)
@@ -293,7 +294,6 @@ class Client:
             try:
                 # update the lifetime datestamp
                 sessions.updateTimestamp(self.session)
-                sessions.commit()
                 user = sessions.get(self.session, 'user')
             except KeyError:
                 # not valid, ignore id
@@ -613,10 +613,9 @@ class Client:
                 self.session = self.session[:-1]
 
         # insert the session in the sessiondb
-        self.db.sessions.set(self.session, user=user, last_use=time.time())
-
-        # and commit immediately
-        self.db.sessions.commit()
+        sessions = self.db.getSessionManager()
+        sessions.set(self.session, user=user)
+        self.db.commit()
 
         # expire us in a long, long time
         expire = Cookie._getdate(86400*365)
index 7560e6a185e662c0341d3c42fca370aad4b13e8a..b14f63bab2923b599e453b3d7623f857c11107e3 100644 (file)
@@ -6,12 +6,10 @@ body.body {
   margin: 0;
 }
 a[href]:hover {
-  background-color: white;
   color:blue;
   text-decoration: underline;
 }
 a[href], a[href]:link {
-  background-color: white;
   color:blue;
   text-decoration: none;
 }
index b2ffcd1e4656aff7c50f8df65f6456d98049506a..2405c54d425734aca23976db511a77d02b5614c7 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.16 2004-03-15 05:50:20 richard Exp $ 
+# $Id: db_test_base.py,v 1.17 2004-03-18 01:58:46 richard Exp $ 
 
 import unittest, os, shutil, errno, imp, sys, time, pprint
 
@@ -55,8 +55,8 @@ class MyTestCase(unittest.TestCase):
     def tearDown(self):
         if hasattr(self, 'db'):
             self.db.close()
-        if os.path.exists('_test_dir'):
-            shutil.rmtree('_test_dir')
+        if os.path.exists(config.DATABASE):
+            shutil.rmtree(config.DATABASE)
 
 class config:
     DATABASE='_test_dir'
diff --git a/test/session_common.py b/test/session_common.py
new file mode 100644 (file)
index 0000000..7029528
--- /dev/null
@@ -0,0 +1,46 @@
+import os, shutil, unittest
+
+from db_test_base import config
+
+class SessionTest(unittest.TestCase):
+    def setUp(self):
+        # remove previous test, ignore errors
+        if os.path.exists(config.DATABASE):
+            shutil.rmtree(config.DATABASE)
+        os.makedirs(config.DATABASE + '/files')
+        self.db = self.module.Database(config, 'admin')
+        self.sessions = self.sessions_module.Sessions(self.db)
+        self.otks = self.sessions_module.OneTimeKeys(self.db)
+
+    def tearDown(self):
+        del self.otks
+        del self.sessions
+        if hasattr(self, 'db'):
+            self.db.close()
+        if os.path.exists(config.DATABASE):
+            shutil.rmtree(config.DATABASE)
+
+    def testSetSession(self):
+        self.sessions.set('random_key', text='hello, world!')
+        self.assertEqual(self.sessions.get('random_key', 'text'),
+            'hello, world!')
+
+    def testUpdateSession(self):
+        self.sessions.set('random_key', text='hello, world!')
+        self.assertEqual(self.sessions.get('random_key', 'text'),
+            'hello, world!')
+        self.sessions.set('random_key', text='nope')
+        self.assertEqual(self.sessions.get('random_key', 'text'), 'nope')
+
+    def testSetOTK(self):
+        assert 0, 'not implemented'
+
+    def testExpiry(self):
+        assert 0, 'not implemented'
+
+class DBMTest(SessionTest):
+    import roundup.backends.sessions_dbm as sessions_module
+
+class RDBMSTest(SessionTest):
+    import roundup.backends.sessions_rdbms as sessions_module
+
index 77c57b8744cf965da4ed77c292f9c1b6ba72c746..cb91c975db008eead4f7c4bdc80f6ce5f48bfe59 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_anydbm.py,v 1.2 2003-11-14 00:11:19 richard Exp $ 
+# $Id: test_anydbm.py,v 1.3 2004-03-18 01:58:46 richard Exp $ 
 
 import unittest, os, shutil, time
 
@@ -39,6 +39,10 @@ class anydbmSchemaTest(anydbmOpener, SchemaTest):
 class anydbmClassicInitTest(ClassicInitTest):
     backend = 'anydbm'
 
+from session_common import DBMTest
+class anydbmSessionTest(anydbmOpener, DBMTest):
+    pass
+
 def test_suite():
     suite = unittest.TestSuite()
     print 'Including anydbm tests'
@@ -46,6 +50,7 @@ def test_suite():
     suite.addTest(unittest.makeSuite(anydbmROTest))
     suite.addTest(unittest.makeSuite(anydbmSchemaTest))
     suite.addTest(unittest.makeSuite(anydbmClassicInitTest))
+    suite.addTest(unittest.makeSuite(anydbmSessionTest))
     return suite
 
 if __name__ == '__main__':
index 24cde202a96188b731a79e3af630a2924cfab477..ef81a3b37612a4186823a17d4e22970f0cc1ecfa 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_bsddb.py,v 1.2 2003-11-14 00:11:19 richard Exp $ 
+# $Id: test_bsddb.py,v 1.3 2004-03-18 01:58:46 richard Exp $ 
 
 import unittest, os, shutil, time
 
@@ -41,6 +41,10 @@ class bsddbSchemaTest(bsddbOpener, SchemaTest):
 class bsddbClassicInitTest(ClassicInitTest):
     backend = 'bsddb'
 
+from session_common import DBMTest
+class bsddbSessionTest(bsddbOpener, DBMTest):
+    pass
+
 def test_suite():
     suite = unittest.TestSuite()
     if not hasattr(backends, 'bsddb'):
@@ -51,6 +55,7 @@ def test_suite():
     suite.addTest(unittest.makeSuite(bsddbROTest))
     suite.addTest(unittest.makeSuite(bsddbSchemaTest))
     suite.addTest(unittest.makeSuite(bsddbClassicInitTest))
+    suite.addTest(unittest.makeSuite(bsddbSessionTest))
     return suite
 
 if __name__ == '__main__':
index 2387cda04aa6aec674b09becb3b3904146060a7d..3469fdda6d56c86a0df79fd618cf2a4c70903a78 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_bsddb3.py,v 1.2 2003-11-14 00:11:19 richard Exp $ 
+# $Id: test_bsddb3.py,v 1.3 2004-03-18 01:58:46 richard Exp $ 
 
 import unittest, os, shutil, time
 
@@ -41,6 +41,10 @@ class bsddb3SchemaTest(bsddb3Opener, SchemaTest):
 class bsddb3ClassicInitTest(ClassicInitTest):
     backend = 'bsddb3'
 
+from session_common import DBMTest
+class bsddb3SessionTest(bsddb3Opener, DBMTest):
+    pass
+
 def test_suite():
     suite = unittest.TestSuite()
     if not hasattr(backends, 'bsddb3'):
@@ -51,6 +55,7 @@ def test_suite():
     suite.addTest(unittest.makeSuite(bsddb3ROTest))
     suite.addTest(unittest.makeSuite(bsddb3SchemaTest))
     suite.addTest(unittest.makeSuite(bsddb3ClassicInitTest))
+    suite.addTest(unittest.makeSuite(bsddb3SessionTest))
     return suite
 
 if __name__ == '__main__':
index 0610b79c540f17b450f6f63959b922f93f30ec26..845f4c291cc40172beda73008539c5e3e428d51c 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_metakit.py,v 1.3 2004-01-27 18:16:50 wc2so1 Exp $ 
+# $Id: test_metakit.py,v 1.4 2004-03-18 01:58:46 richard Exp $ 
 import unittest, os, shutil, time, weakref
 
 from db_test_base import DBTest, ROTest, SchemaTest, ClassicInitTest, config, password
@@ -90,6 +90,10 @@ class metakitSchemaTest(metakitOpener, SchemaTest):
 class metakitClassicInitTest(ClassicInitTest):
     backend = 'metakit'
 
+from session_common import DBMTest
+class metakitSessionTest(metakitOpener, DBMTest):
+    pass
+
 def test_suite():
     suite = unittest.TestSuite()
     if not hasattr(backends, 'metakit'):
@@ -100,6 +104,7 @@ def test_suite():
     suite.addTest(unittest.makeSuite(metakitROTest))
     suite.addTest(unittest.makeSuite(metakitSchemaTest))
     suite.addTest(unittest.makeSuite(metakitClassicInitTest))
+    suite.addTest(unittest.makeSuite(metakitSessionTest))
     return suite
 
 if __name__ == '__main__':
index 54f7dcf6ffb7a739390585c640f228acef401303..1fdc9417dfc628fa6f05eb5a32c3b9d415450eb9 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_mysql.py,v 1.7 2004-03-12 04:09:00 richard Exp $ 
+# $Id: test_mysql.py,v 1.8 2004-03-18 01:58:46 richard Exp $ 
 
 import unittest, os, shutil, time, imp
 
@@ -78,6 +78,15 @@ MYSQL_DATABASE = (MYSQL_DBHOST, MYSQL_DBUSER, MYSQL_DBPASSWORD, MYSQL_DBNAME)
         ClassicInitTest.tearDown(self)
         self.nuke_database()
 
+from session_common import RDBMSTest
+class mysqlSessionTest(mysqlOpener, RDBMSTest):
+    def setUp(self):
+        mysqlOpener.setUp(self)
+        RDBMSTest.setUp(self)
+    def tearDown(self):
+        RDBMSTest.tearDown(self)
+        mysqlOpener.tearDown(self)
+
 def test_suite():
     suite = unittest.TestSuite()
     if not hasattr(backends, 'mysql'):
@@ -97,6 +106,7 @@ def test_suite():
         suite.addTest(unittest.makeSuite(mysqlROTest))
         suite.addTest(unittest.makeSuite(mysqlSchemaTest))
         suite.addTest(unittest.makeSuite(mysqlClassicInitTest))
+        suite.addTest(unittest.makeSuite(mysqlSessionTest))
     return suite
 
 if __name__ == '__main__':
index ac628be2e6ef16269c81cfcf411d2fef3e633919..81ce5dcb5b4b439c697231ddb778ca839c4e0882 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_postgresql.py,v 1.5 2004-03-12 04:09:00 richard Exp $ 
+# $Id: test_postgresql.py,v 1.6 2004-03-18 01:58:46 richard Exp $ 
 
 import unittest
 
@@ -92,6 +92,15 @@ class postgresqlClassicInitTest(postgresqlOpener, ClassicInitTest):
         ClassicInitTest.tearDown(self)
         postgresqlOpener.tearDown(self)
 
+from session_common import RDBMSTest
+class postgresqlSessionTest(postgresqlOpener, RDBMSTest):
+    def setUp(self):
+        postgresqlOpener.setUp(self)
+        RDBMSTest.setUp(self)
+    def tearDown(self):
+        RDBMSTest.tearDown(self)
+        postgresqlOpener.tearDown(self)
+
 def test_suite():
     suite = unittest.TestSuite()
     if not hasattr(backends, 'postgresql'):
@@ -106,5 +115,6 @@ def test_suite():
     suite.addTest(unittest.makeSuite(postgresqlROTest))
     suite.addTest(unittest.makeSuite(postgresqlSchemaTest))
     suite.addTest(unittest.makeSuite(postgresqlClassicInitTest))
+    suite.addTest(unittest.makeSuite(postgresqlSessionTest))
     return suite
 
index 19e4277459ce349160ae44857ec6edd8c651cf59..85f5184eaa0f8c308373aa9d4611b16a4cf9124f 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_sqlite.py,v 1.3 2003-11-14 00:11:19 richard Exp $ 
+# $Id: test_sqlite.py,v 1.4 2004-03-18 01:58:46 richard Exp $ 
 
 import unittest, os, shutil, time
 
@@ -41,6 +41,10 @@ class sqliteSchemaTest(sqliteOpener, SchemaTest):
 class sqliteClassicInitTest(ClassicInitTest):
     backend = 'sqlite'
 
+from session_common import RDBMSTest
+class sqliteSessionTest(sqliteOpener, RDBMSTest):
+    pass
+
 def test_suite():
     suite = unittest.TestSuite()
     from roundup import backends
@@ -52,6 +56,7 @@ def test_suite():
     suite.addTest(unittest.makeSuite(sqliteROTest))
     suite.addTest(unittest.makeSuite(sqliteSchemaTest))
     suite.addTest(unittest.makeSuite(sqliteClassicInitTest))
+    suite.addTest(unittest.makeSuite(sqliteSessionTest))
     return suite
 
 if __name__ == '__main__':