Index: /elixir/trunk/tests/test_versioning.py
===================================================================
--- /elixir/trunk/tests/test_versioning.py (revision 172)
+++ /elixir/trunk/tests/test_versioning.py (revision 172)
@@ -0,0 +1,107 @@
+from elixir import *
+from elixir.ext.versioned import acts_as_versioned
+from datetime import datetime, timedelta
+
+import time
+
+
+def setup():
+    global Director, Movie, Actor
+
+    class Director(Entity):
+        has_field('name', Unicode(60))
+        has_many('movies', of_kind='Movie', inverse='director')
+        using_options(tablename='directors')
+
+
+    class Movie(Entity):
+        has_field('id', Integer, primary_key=True)
+        has_field('title', Unicode(60), primary_key=True)
+        has_field('description', Unicode(512))
+        has_field('releasedate', DateTime)
+        belongs_to('director', of_kind='Director', inverse='movies')
+        has_and_belongs_to_many('actors', of_kind='Actor', inverse='movies', tablename='movie_casting')
+        using_options(tablename='movies')
+        acts_as_versioned()
+
+
+    class Actor(Entity):
+        has_field('name', Unicode(60))
+        has_and_belongs_to_many('movies', of_kind='Movie', inverse='actors', tablename='movie_casting')
+        using_options(tablename='actors')
+
+    metadata.bind = 'sqlite:///'
+
+
+def teardown():
+    cleanup_all()
+
+
+class TestVersioning(object):
+    def setup(self):
+        create_all()
+    
+    def teardown(self):
+        drop_all()
+        objectstore.clear()
+    
+    def test_versioning(self):    
+        gilliam = Director(name='Terry Gilliam')
+        monkeys = Movie(id=1, title='12 Monkeys', description='draft description', director=gilliam)
+        bruce = Actor(name='Bruce Willis', movies=[monkeys])
+        objectstore.flush(); objectstore.clear()
+    
+        time.sleep(1)
+        after_create = datetime.now()
+        time.sleep(1)
+    
+        movie = Movie.get_by(title='12 Monkeys')
+        assert movie.version == 1
+        assert movie.title == '12 Monkeys'
+        assert movie.director.name == 'Terry Gilliam'
+        movie.description = 'description two'
+        objectstore.flush(); objectstore.clear()
+    
+        time.sleep(1)
+        after_update_one = datetime.now()
+        time.sleep(1)
+    
+        movie = Movie.get_by(title='12 Monkeys')
+        movie.description = 'description three'
+        objectstore.flush(); objectstore.clear()
+    
+        time.sleep(1)
+        after_update_two = datetime.now()
+        time.sleep(1)
+    
+        movie = Movie.get_by(title='12 Monkeys')
+        oldest_version = movie.get_as_of(after_create)
+        middle_version = movie.get_as_of(after_update_one)
+        latest_version = movie.get_as_of(after_update_two)
+    
+        initial_timestamp = oldest_version.timestamp
+    
+        assert oldest_version.version == 1
+        assert oldest_version.description == 'draft description'
+    
+        assert middle_version.version == 2
+        assert middle_version.description == 'description two'
+    
+        assert latest_version.version == 3
+        assert latest_version.description == 'description three'
+    
+        differences = latest_version.compare_with(oldest_version)
+        assert differences['description'] == ('description three', 'draft description')
+    
+        assert len(movie.versions) == 2
+        assert movie.versions[0] == oldest_version
+        assert movie.versions[1] == middle_version
+    
+        movie.revert_to(1)
+        objectstore.flush(); objectstore.clear()
+    
+        movie = Movie.get_by(title='12 Monkeys')
+        assert movie.version == 1
+        assert movie.timestamp == initial_timestamp
+        assert movie.title == '12 Monkeys'
+        assert movie.director.name == 'Terry Gilliam'
Index: /elixir/trunk/tests/test_encryption.py
===================================================================
--- /elixir/trunk/tests/test_encryption.py (revision 172)
+++ /elixir/trunk/tests/test_encryption.py (revision 172)
@@ -0,0 +1,71 @@
+from elixir import *
+from elixir.ext.encrypted import acts_as_encrypted
+ 
+
+def setup():
+    global Person, Pet
+    
+    class Person(Entity):
+        has_field('name', Unicode)
+        has_field('password', Unicode)
+        has_field('ssn', Unicode)
+        has_many('pets', of_kind='Pet')
+        acts_as_encrypted(for_fields=['password', 'ssn'], with_secret='secret')
+
+    class Pet(Entity):
+        has_field('name', Unicode)
+        has_field('codename', Unicode)
+        acts_as_encrypted(for_fields=['codename'], with_secret='secret2')
+        belongs_to('owner', of_kind='Person')
+
+    
+    metadata.bind = 'sqlite:///'
+
+
+def teardown():
+    cleanup_all()
+
+
+class TestEncryption(object):
+    def setup(self):
+        create_all()
+    
+    def teardown(self):
+        drop_all()
+        objectstore.clear()
+    
+    def test_encryption(self):    
+        jonathan = Person(
+            name=u'Jonathan LaCour', 
+            password=u's3cr3tw0RD', 
+            ssn=u'123-45-6789'
+        )
+        winston = Pet(
+            name='Winston',
+            codename='Pug/Shih-Tzu Mix'
+        )
+        nelson = Pet(
+            name='Nelson',
+            codename='Pug'
+        )
+        jonathan.pets = [winston, nelson]
+
+        objectstore.flush(); objectstore.clear()
+
+        p = Person.q.get_by(name='Jonathan LaCour')
+        assert p.password == 's3cr3tw0RD'
+        assert p.ssn == '123-45-6789'
+        assert p.pets[0].name == 'Winston'
+        assert p.pets[0].codename == 'Pug/Shih-Tzu Mix'
+        assert p.pets[1].name == 'Nelson'
+        assert p.pets[1].codename == 'Pug'
+
+        p.password = 'N3wpAzzw0rd'
+
+        objectstore.flush(); objectstore.clear()
+
+        p = Person.q.get_by(name='Jonathan LaCour')
+        assert p.password == 'N3wpAzzw0rd'
+        p.name = 'Jon LaCour'
+
+        objectstore.flush(); objectstore.clear()
Index: /elixir/trunk/elixir/ext/associable.py
===================================================================
--- /elixir/trunk/elixir/ext/associable.py (revision 140)
+++ /elixir/trunk/elixir/ext/associable.py (revision 172)
@@ -168,5 +168,8 @@
             field.deferred = False
             field.primary_key = False
-            field.column = sa.Column('%s_assoc_id' % interface_name, None, 
+            # CHANGE: I had to change the second argument from None to sa.Integer
+            # in order to get associable working with the versioning extension...
+            # Ben: was this the right thing to do?
+            field.column = sa.Column('%s_assoc_id' % interface_name, sa.Integer, 
                                   sa.ForeignKey('%s.%s_id' % (able_name, able_name)))
             entity._descriptor.add_field(field)
Index: /elixir/trunk/elixir/ext/encrypted.py
===================================================================
--- /elixir/trunk/elixir/ext/encrypted.py (revision 172)
+++ /elixir/trunk/elixir/ext/encrypted.py (revision 172)
@@ -0,0 +1,86 @@
+'''
+An encryption plugin for Elixir utilizing the excellent PyCrypto library, which
+can be downloaded here: http://www.amk.ca/python/code/crypto
+
+Values for columns that are specified to be encrypted will be transparently
+encrypted and safely encoded for storage in a unicode column using the powerful
+and secure Blowfish Cipher using a specified "secret" which can be passed into
+the plugin at class declaration time.
+
+Example usage:
+
+    from elixir import *
+    from elixir.ext.encrypted import acts_as_encrypted
+    
+    class Person(Entity):
+        has_field('name', Unicode)
+        has_field('password', Unicode)
+        has_field('ssn', Unicode)
+        acts_as_encrypted(for_columns=['password', 'ssn'], with_secret='secret')
+
+The above Person entity will automatically encrypt and decrypt the password and
+ssn columns on save, update, and load.  Different secrets can be specified on
+an entity by entity basis, for added security.
+'''
+
+from Crypto.Cipher          import Blowfish 
+from elixir.statements      import Statement
+from sqlalchemy.orm.mapper  import MapperExtension, EXT_PASS
+
+
+#
+# encryption and decryption functions
+#
+
+def encrypt_value(value, secret):
+    return Blowfish.new(secret, Blowfish.MODE_CFB).encrypt(value).encode('string_escape')
+
+def decrypt_value(value, secret):
+    return Blowfish.new(secret, Blowfish.MODE_CFB).decrypt(value.decode('string_escape'))
+
+
+#
+# acts_as_encrypted statement
+#
+
+class ActsAsEncrypted(object):    
+    def __init__(self, entity, for_fields=[], with_secret='abcdef'):
+        
+        def perform_encryption(instance, decrypt=False):
+            for column_name in for_fields:
+                current_value = getattr(instance, column_name)
+                if current_value:
+                    if decrypt:
+                        new_value = decrypt_value(current_value, with_secret)
+                    else:
+                        new_value = encrypt_value(current_value, with_secret)
+                    setattr(instance, column_name, new_value)
+        
+        def perform_decryption(instance):
+            perform_encryption(instance, decrypt=True)
+        
+        class EncryptedMapperExtension(MapperExtension):            
+            
+            def before_insert(self, mapper, connection, instance):
+                perform_encryption(instance)
+                return EXT_PASS
+            
+            def before_update(self, mapper, connection, instance):       
+                perform_encryption(instance)
+                return EXT_PASS
+            
+            def populate_instance(self, mapper, selectcontext, row, instance, identitykey, isnew):
+                mapper.populate_instance(selectcontext, instance, row, identitykey, isnew)
+                perform_decryption(instance)
+                return True
+        
+        # make sure that the entity's mapper has our mapper extension
+        entity._descriptor.add_mapper_extension(EncryptedMapperExtension())
+
+
+acts_as_encrypted = Statement(ActsAsEncrypted)
+
+
+__all__ = [
+    'acts_as_encrypted'
+]
Index: /elixir/trunk/elixir/ext/versioned.py
===================================================================
--- /elixir/trunk/elixir/ext/versioned.py (revision 172)
+++ /elixir/trunk/elixir/ext/versioned.py (revision 172)
@@ -0,0 +1,208 @@
+'''
+A versioning plugin for Elixir.
+
+Entities that are marked as versioned will automatically have a history table
+created and a timestamp and version column added to their tables. In addition,
+versioned entities are provided with four new methods: revert, revert_to, and
+compare_with, get_as_of, and one new attribute: versions.  Entities with 
+compound primary keys are supported.
+
+The `versions` attribute will contain a list of previous versions of the
+instance, in increasing version number order.
+
+The `get_as_of` method will retrieve a previous version of the instance "as of"
+a specified datetime. If the current version is the most recent, it will be
+returned.
+
+The `revert` method will rollback the current instance to its previous version,
+if possible. Once reverted, the current instance will be expired from the
+session, and you will need to fetch it again to retrieve the now reverted
+instance.
+
+The `revert_to` method will rollback the current instance to the specified
+version number, if possibe. Once reverted, the current instance will be expired
+from the session, and you will need to fetch it again to retrieve the now
+reverted instance.
+
+The `compare_with` method will compare the instance with a previous version. A
+dictionary will be returned with each field difference as an element in the
+dictionary where the key is the field name and the value is a tuple of the
+format (current_value, version_value). Version instances also have a
+`compare_with` method so that two versions can be compared.
+
+Also included in the module is a `after_revert` decorator that can be used to
+decorate methods on the versioned entity that will be called following that 
+instance being reverted.
+
+Note that relationships that are stored in mapping tables will not be included
+as part of the versioning process, and will need to be handled manually. Only
+values within the entity's main table will be versioned into the history table.
+'''
+
+from elixir                import Integer, objectstore, DateTime
+from elixir.statements     import Statement
+from elixir.fields         import Field
+from sqlalchemy.orm.mapper import MapperExtension, EXT_PASS
+from sqlalchemy            import Table, Column, mapper, and_, desc
+from datetime              import datetime
+
+import inspect
+
+
+#
+# utility functions
+#
+
+def get_entity_where(instance):
+    clauses = []
+    for column in instance.table.primary_key.columns:
+        instance_value = getattr(instance, column.name)
+        clauses.append(column==instance_value)
+    return and_(*clauses)
+
+
+def get_history_where(instance):
+    clauses = []
+    for column in instance.table.primary_key.columns:
+        instance_value = getattr(instance, column.name)
+        history_column = getattr(instance.__history_table__.primary_key.columns, column.name)
+        clauses.append(history_column==instance_value)
+    return and_(*clauses)
+
+
+#
+# a mapper extension to track versions on insert, update, and delete
+#
+
+class VersionedMapperExtension(MapperExtension):
+    def before_insert(self, mapper, connection, instance):
+        instance.version = 1
+        instance.timestamp = datetime.now()
+        return EXT_PASS
+    
+    def before_update(self, mapper, connection, instance):        
+        values = instance.table.select(get_entity_where(instance)).execute().fetchone()
+        colvalues = dict(values.items())
+        instance.__class__.__history_table__.insert().execute(colvalues)
+        instance.version += 1
+        instance.timestamp = datetime.now()
+        return EXT_PASS
+    
+    def before_delete(self, mapper, connection, instance):
+        instance.__history_table__.delete(
+            instance.__history_table__.c.id==instance.id
+        ).execute()
+        return EXT_PASS
+
+
+versioned_mapper_extension = VersionedMapperExtension()
+
+
+#
+# the acts_as_versioned statement
+#
+
+class ActsAsVersioned(object):
+        
+    def __init__(self, entity):
+        entity._descriptor.add_mapper_extension(versioned_mapper_extension)
+        
+        # add a version field to the entity, along with a timestamp
+        versionField = Field(Integer, colname='version')
+        timestampField = Field(DateTime, colname='timestamp')
+        entity._descriptor.add_field(versionField)
+        entity._descriptor.add_field(timestampField)
+    
+    @classmethod
+    def finalize(cls, entity):
+        # look for events
+        after_revert_events = []
+        for name, func in inspect.getmembers(entity, inspect.ismethod):
+            if getattr(func, '_elixir_after_revert', False):
+                after_revert_events.append(func)
+        
+        # create a history table for the entity
+        columns = [ column.copy() for column in entity.table.c if column.name != 'version' ]
+        columns.append(Column('version', Integer, primary_key=True))
+        table = Table(entity.table.name + '_history', entity.table.metadata, 
+            *columns
+        )
+        entity.__history_table__ = table
+        
+        # create an object that represents a version of this entity
+        class Version(object):
+            pass
+            
+        # map the version class to the history table for this entity
+        Version.__name__ = entity.__name__ + 'Version'
+        Version.__versioned_entity__ = entity
+        mapper(Version, entity.__history_table__)
+                        
+        # attach utility methods and properties to the entity
+        def get_versions(self):
+            return objectstore.query(Version).select(get_history_where(self))
+        
+        def get_as_of(self, dt):
+            # if the passed in timestamp is older than our current version's
+            # time stamp, then the most recent version is our current version
+            if self.timestamp < dt: return self
+            
+            # otherwise, we need to look to the history table to get our
+            # older version
+            items = objectstore.query(Version).select(
+                and_(get_history_where(self), Version.c.timestamp <= dt),
+                order_by=desc(Version.c.timestamp),
+                limit=1
+            )
+            if items: return items[0]
+            else: return None
+        
+        def revert_to(self, to_version):
+            hist = entity.__history_table__
+            old_version = hist.select(and_(
+                get_history_where(self), 
+                hist.c.version==to_version
+            )).execute().fetchone()
+            
+            entity.table.update(get_entity_where(self)).execute(
+                **dict(old_version.items())
+            )
+            
+            hist.delete(and_(get_history_where(self), hist.c.version>=to_version)).execute()
+            for event in after_revert_events: event(self)
+        
+        def revert(self):
+            assert self.version > 1
+            self.revert_to(self.version - 1)
+            
+        def compare_with(self, version):
+            differences = {}
+            for column in self.c:
+                if column.name == 'version': continue
+                this = getattr(self, column.name)
+                that = getattr(version, column.name)
+                if this != that:
+                    differences[column.name] = (this, that)
+            return differences
+        
+        entity.versions      = property(get_versions)
+        entity.get_as_of     = get_as_of
+        entity.revert_to     = revert_to
+        entity.revert        = revert
+        entity.compare_with  = compare_with
+        Version.compare_with = compare_with
+
+
+acts_as_versioned = Statement(ActsAsVersioned)
+
+
+#
+# decorator for watching for revert events
+#
+
+def after_revert(func):
+    func._elixir_after_revert = True
+    return func
+
+
+__all__ = ['acts_as_versioned', 'after_revert']
