Author: chrisz
Date: Mon Jan 17 06:36:05 2011
New Revision: 7205
URL: http://trac.turbogears.org/changeset/7205

Log:
Added default identity model classes/tables to the SQLAlchemy Identity provider 
(ticket #1453).

Modified:
   branches/1.5/turbogears/identity/saprovider.py
   branches/1.5/turbogears/identity/soprovider.py
   branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl
   branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl
   branches/1.5/turbogears/visit/savisit.py

Modified: branches/1.5/turbogears/identity/saprovider.py
==============================================================================
--- branches/1.5/turbogears/identity/saprovider.py      Mon Jan 17 03:33:27 
2011        (r7204)
+++ branches/1.5/turbogears/identity/saprovider.py      Mon Jan 17 06:36:05 
2011        (r7205)
@@ -1,16 +1,24 @@
 import logging
+from datetime import datetime
 
 from turbogears import config, identity
-from turbogears.database import bind_metadata, session
+from turbogears.database import bind_metadata, metadata, session
 from turbogears.util import load_class
+from turbojson.jsonify import jsonify_saobject, jsonify
 
-from sqlalchemy.orm import class_mapper
+from sqlalchemy import (Table, Column, ForeignKey,
+    String, Unicode, Integer, DateTime)
+from sqlalchemy.orm import class_mapper, mapper, relation
 try:
     from sqlalchemy.exc import IntegrityError
 except ImportError: # SQLAlchemy < 0.5
     from sqlalchemy.exceptions import IntegrityError
+try:
+    from sqlalchemy.orm.exc import UnmappedClassError
+except ImportError: # SQLAlchemy < 0.5
+    from sqlalchemy.exceptions import InvalidRequestError as UnmappedClassError
 
-log = logging.getLogger("turbogears.identity.saprovider")
+log = logging.getLogger('turbogears.identity.saprovider')
 
 
 # Global class references --
@@ -157,10 +165,15 @@
         for classname in ('user', 'group', 'permission', 'visit'):
             default_classname = '.TG_' + (classname == 'visit'
                 and 'VisitIdentity' or classname.capitalize())
-            class_path = config.get("identity.saprovider.model.%s" % classname,
+            class_path = config.get('identity.saprovider.model.%s' % classname,
                 __name__ + default_classname)
             class_ = load_class(class_path)
             if class_:
+                if class_path == __name__ + default_classname:
+                    try:
+                        class_mapper(class_)
+                    except UnmappedClassError:
+                        class_._map()
                 log.info('Successfully loaded "%s".', class_path)
                 glob_ns['%s_class' % classname] = class_
             else:
@@ -169,7 +182,7 @@
 
     def encrypt_password(self, password):
         # Default encryption algorithm is to use plain text passwords
-        algorithm = config.get("identity.saprovider.encryption_algorithm", 
None)
+        algorithm = config.get('identity.saprovider.encryption_algorithm', 
None)
         return identity.encrypt_pw_with_algorithm(algorithm, password)
 
     def create_provider_model(self):
@@ -177,7 +190,11 @@
         bind_metadata()
         class_mapper(user_class).local_table.create(checkfirst=True)
         class_mapper(group_class).local_table.create(checkfirst=True)
+        if group_class is TG_Group:
+            group_class._user_association_table.create(checkfirst=True)
         class_mapper(permission_class).local_table.create(checkfirst=True)
+        if permission_class is TG_Permission:
+            permission_class._group_association_table.create(checkfirst=True)
         class_mapper(visit_class).local_table.create(checkfirst=True)
 
     def validate_identity(self, user_name, password, visit_key):
@@ -245,3 +262,190 @@
     def authenticated_identity(self, user):
         """Construct Identity object for users with no visit_key."""
         return SqlAlchemyIdentity(user=user)
+
+
+# default identity model classes
+
+
+class TG_User(object):
+    """Reasonably basic User definition."""
+
+    def __repr__(self):
+        return '<User: name="%s", email="%s", display name="%s">' % (
+            self.user_name, self.email_address, self.display_name)
+
+    def __unicode__(self):
+        return self.display_name or self.user_name
+
+    @property
+    def permissions(self):
+        """Return all permissions of all groups the user belongs to."""
+        p = set()
+        for g in self.groups:
+            p |= set(g.permissions)
+        return p
+
+    @classmethod
+    def by_email_address(cls, email_address):
+        return 
session.query(cls).filter_by(email_address=email_address).first()
+
+    @classmethod
+    def by_user_name(cls, user_name):
+        return session.query(cls).filter_by(user_name=user_name).first()
+    by_name = by_user_name
+
+    def _set_password(self, password):
+        """Run cleartext password through the hash algorithm before saving."""
+        try:
+            hash = 
identity.current_provider.encrypt_password(cleartext_password)
+        except identity.exceptions.IdentityManagementNotEnabledException:
+            # Creating identity provider just to encrypt password
+            # (so we don't reimplement the encryption step).
+            ip = SqlAlchemyIdentityProvider()
+            hash = ip.encrypt_password(cleartext_password)
+            if hash == cleartext_password:
+                log.info("Identity provider not enabled,"
+                    " and no encryption algorithm specified in config."
+                    " Setting password as plaintext.")
+        self._password = hash
+
+    def _get_password(self):
+        """Returns password."""
+        return self._password
+
+    password = property(_get_password, _set_password)
+
+    @classmethod
+    def _map(cls):
+        cls._table = Table('tg_user', metadata,
+            Column('user_id', Integer, primary_key=True),
+            Column('user_name', Unicode(16), unique=True, nullable=False),
+            Column('email_address', Unicode(255), unique=True),
+            Column('display_name', Unicode(255)),
+            Column('password', Unicode(40)),
+            Column('created', DateTime, default=datetime.now))
+        cls._mapper = mapper(cls, cls._table,
+            properties=dict(_password=cls._table.c.password))
+
[email protected]('isinstance(obj, TG_User)')
+def jsonify_user(obj):
+    """Convert user to JSON."""
+    result = jsonify_saobject(obj)
+    result.pop('password', None)
+    result.pop('_password', None)
+    result['groups'] = [g.group_name for g in obj.groups]
+    result['permissions'] = [p.permission_name for p in obj.permissions]
+    return result
+
+
+class TG_Group(object):
+    """An ultra-simple Group definition."""
+
+    def __repr__(self):
+        return '<Group: name="%s", display_name="%s">' % (
+            self.group_name, self.display_name)
+
+    def __unicode__(self):
+        return self.display_name or self.group_name
+
+    @classmethod
+    def by_group_name(cls, group_name):
+        """Look up Group by given group name."""
+        return session.query(cls).filter_by(group_name=group_name).first()
+    by_name = by_group_name
+
+    @classmethod
+    def _map(cls):
+        cls._table = Table('tg_group', metadata,
+            Column('group_id', Integer, primary_key=True),
+            Column('group_name', Unicode(16), unique=True, nullable=False),
+            Column('display_name', Unicode(255)),
+            Column('created', DateTime, default=datetime.now))
+        cls._user_association_table = Table('user_group', metadata,
+            Column('user_id', Integer, ForeignKey('tg_user.user_id',
+                onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
+            Column('group_id', Integer, ForeignKey('tg_group.group_id',
+                onupdate='CASCADE', ondelete='CASCADE'), primary_key=True))
+        cls._mapper = mapper(cls, cls._table,
+            properties=dict(users=relation(TG_User,
+                secondary=cls._user_association_table, backref='groups')))
+
[email protected]('isinstance(obj, TG_Group)')
+def jsonify_group(obj):
+    """Convert group to JSON."""
+    result = jsonify_saobject(obj)
+    result['users'] = [u.user_name for u in obj.users]
+    result['permissions'] = [p.permission_name for p in obj.permissions]
+    return result
+
+
+class TG_Permission(object):
+    """A relationship that determines what each Group can do."""
+
+    def __repr__(self):
+        return '<Permission: name="%s">' % self.permission_name
+
+    def __unicode__(self):
+        return self.permission_name
+
+    @classmethod
+    def by_permission_name(cls, permission_name):
+        """Look up Permission by given permission name."""
+        return 
session.query(cls).filter_by(permission_name=permission_name).first()
+    by_name = by_permission_name
+
+    @classmethod
+    def _map(cls):
+        cls._table = Table('permission', metadata,
+            Column('permission_id', Integer, primary_key=True),
+            Column('permission_name', Unicode(16), unique=True, 
nullable=False),
+            Column('description', Unicode(255)))
+        cls._group_association_table = Table('group_permission', metadata,
+            Column('group_id', Integer, ForeignKey('tg_group.group_id',
+                onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
+            Column('permission_id',
+                Integer, ForeignKey('permission.permission_id',
+                onupdate='CASCADE', ondelete='CASCADE'), primary_key=True))
+        cls._mapper = mapper(cls, cls._table,
+            properties=dict(groups=relation(TG_Group,
+                secondary=cls._group_association_table, 
backref='permissions')))
+
[email protected]('isinstance(obj, TG_Permission)')
+def jsonify_permission(obj):
+    """Convert permissions to JSON."""
+    result = jsonify_saobject(obj)
+    result['groups'] = [g.group_name for g in obj.groups]
+    return result
+
+
+class TG_VisitIdentity(object):
+    """A Visit that is linked to a User object."""
+
+    @classmethod
+    def by_visit_key(cls, visit_key):
+        """Look up VisitIdentity by given visit key."""
+        return session.query(cls).get(visit_key)
+
+    @classmethod
+    def _map(cls):
+        cls._table = Table('visit_identity', metadata,
+            Column('visit_key', String(40), primary_key=True),
+            Column('user_id', Integer,
+                ForeignKey('tg_user.user_id'), index=True))
+        cls._mapper = mapper(cls, cls._table,
+            properties=dict(user=relation(TG_User, backref='visit_identity')))
+
+
+def encrypt_password(cleartext_password):
+    """Encrypt given cleartext password."""
+    try:
+        hash = identity.current_provider.encrypt_password(cleartext_password)
+    except identity.exceptions.RequestRequiredException:
+        # Creating identity provider just to encrypt password
+        # (so we don't reimplement the encryption step).
+        ip = SqlAlchemyIdentityProvider()
+        hash = ip.encrypt_password(cleartext_password)
+        if hash == cleartext_password:
+            log.info("Identity provider not enabled, and no encryption "
+                "algorithm specified in config. Setting password as 
plaintext.")
+    return hash

Modified: branches/1.5/turbogears/identity/soprovider.py
==============================================================================
--- branches/1.5/turbogears/identity/soprovider.py      Mon Jan 17 03:33:27 
2011        (r7204)
+++ branches/1.5/turbogears/identity/soprovider.py      Mon Jan 17 06:36:05 
2011        (r7205)
@@ -11,9 +11,9 @@
 from sqlobject.dberrors import DuplicateEntryError
 
 
-log = logging.getLogger("turbogears.identity.soprovider")
+log = logging.getLogger('turbogears.identity.soprovider')
 
-hub = PackageHub("turbogears.identity")
+hub = PackageHub('turbogears.identity')
 __connection__ = hub
 
 
@@ -177,7 +177,7 @@
         for classname in ('user', 'group', 'permission', 'visit'):
             default_classname = '.TG_' + (classname == 'visit'
                 and 'VisitIdentity' or classname.capitalize())
-            class_path = config.get("identity.soprovider.model.%s" % classname,
+            class_path = config.get('identity.soprovider.model.%s' % classname,
                 __name__ + default_classname)
             class_ = load_class(class_path)
             if class_:
@@ -194,7 +194,7 @@
 
     def encrypt_password(self, password):
         # Default encryption algorithm is to use plain text passwords
-        algorithm = config.get("identity.soprovider.encryption_algorithm", 
None)
+        algorithm = config.get('identity.soprovider.encryption_algorithm', 
None)
         return identity.encrypt_pw_with_algorithm(algorithm, password)
 
     def create_provider_model(self):
@@ -279,57 +279,20 @@
         return SqlObjectIdentity(user=user)
 
 
-class TG_VisitIdentity(SQLObject):
-    """A visit to your website."""
-
-    class sqlmeta:
-        table = "visit_identity"
-
-    visit_key = StringCol(length=40, alternateID=True,
-        alternateMethodName="by_visit_key")
-    user_id = IntCol()
-
-
-class TG_Group(SQLObject):
-    """An ultra-simple group definition."""
-
-    group_name = UnicodeCol(length=16, alternateID=True,
-        alternateMethodName="by_group_name")
-    display_name = UnicodeCol(length=255)
-    created = DateTimeCol(default=datetime.now)
-
-    # collection of all users belonging to this group
-    users = RelatedJoin("TG_User", intermediateTable="user_group",
-        joinColumn="group_id", otherColumn="user_id")
-
-    # collection of all permissions for this group
-    permissions = RelatedJoin("TG_Permission", joinColumn="group_id",
-        intermediateTable="group_permission",
-        otherColumn="permission_id")
-
[email protected]('isinstance(obj, TG_Group)')
-def jsonify_group(obj):
-    """Convert group to JSON."""
-    result = jsonify_sqlobject(obj)
-    result["users"] = [u.user_name for u in obj.users]
-    result["permissions"] = [p.permission_name for p in obj.permissions]
-    return result
-
-
 class TG_User(SQLObject):
     """Reasonably basic User definition."""
 
     user_name = UnicodeCol(length=16, alternateID=True,
-        alternateMethodName="by_user_name")
+        alternateMethodName='by_user_name')
     email_address = UnicodeCol(length=255, alternateID=True,
-        alternateMethodName="by_email_address")
+        alternateMethodName='by_email_address')
     display_name = UnicodeCol(length=255)
     password = UnicodeCol(length=40)
     created = DateTimeCol(default=datetime.now)
 
     # groups this user belongs to
-    groups = RelatedJoin("TG_Group", intermediateTable="user_group",
-        joinColumn="user_id", otherColumn="group_id")
+    groups = RelatedJoin('TG_Group', intermediateTable='user_group',
+        joinColumn='user_id', otherColumn='group_id')
 
     def _get_permissions(self):
         perms = set()
@@ -360,9 +323,35 @@
 def jsonify_user(obj):
     """Convert user to JSON."""
     result = jsonify_sqlobject(obj)
-    del result['password']
-    result["groups"] = [g.group_name for g in obj.groups]
-    result["permissions"] = [p.permission_name for p in obj.permissions]
+    result.pop('password', None)
+    result['groups'] = [g.group_name for g in obj.groups]
+    result['permissions'] = [p.permission_name for p in obj.permissions]
+    return result
+
+
+class TG_Group(SQLObject):
+    """An ultra-simple group definition."""
+
+    group_name = UnicodeCol(length=16, alternateID=True,
+        alternateMethodName='by_group_name')
+    display_name = UnicodeCol(length=255)
+    created = DateTimeCol(default=datetime.now)
+
+    # collection of all users belonging to this group
+    users = RelatedJoin('TG_User', intermediateTable='user_group',
+        joinColumn='group_id', otherColumn='user_id')
+
+    # collection of all permissions for this group
+    permissions = RelatedJoin('TG_Permission', joinColumn='group_id',
+        intermediateTable='group_permission',
+        otherColumn='permission_id')
+
[email protected]('isinstance(obj, TG_Group)')
+def jsonify_group(obj):
+    """Convert group to JSON."""
+    result = jsonify_sqlobject(obj)
+    result['users'] = [u.user_name for u in obj.users]
+    result['permissions'] = [p.permission_name for p in obj.permissions]
     return result
 
 
@@ -370,23 +359,34 @@
     """Permissions for a given group."""
 
     class sqlmeta:
-        table = "permission"
+        table = 'permission'
 
     permission_name = UnicodeCol(length=16, alternateID=True,
-        alternateMethodName="by_permission_name")
+        alternateMethodName='by_permission_name')
     description = UnicodeCol(length=255)
 
-    groups = RelatedJoin("TG_Group", intermediateTable="group_permission",
-        joinColumn="permission_id", otherColumn="group_id")
+    groups = RelatedJoin('TG_Group', intermediateTable='group_permission',
+        joinColumn='permission_id', otherColumn='group_id')
 
 @jsonify.when('isinstance(obj, TG_Permission)')
 def jsonify_permission(obj):
     """Convert permissions to JSON."""
     result = jsonify_sqlobject(obj)
-    result["groups"] = [g.group_name for g in obj.groups]
+    result['groups'] = [g.group_name for g in obj.groups]
     return result
 
 
+class TG_VisitIdentity(SQLObject):
+    """A visit to your website."""
+
+    class sqlmeta:
+        table = 'visit_identity'
+
+    visit_key = StringCol(length=40, alternateID=True,
+        alternateMethodName='by_visit_key')
+    user_id = IntCol()
+
+
 def encrypt_password(cleartext_password):
     """Encrypt given cleartext password."""
     try:

Modified: branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl
==============================================================================
--- branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl       
Mon Jan 17 03:33:27 2011        (r7204)
+++ branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl       
Mon Jan 17 06:36:05 2011        (r7205)
@@ -32,8 +32,8 @@
     #elif $identity == "sqlalchemy"
     result = jsonify_saobject(obj)
     #end if
-    result["users"] = [u.user_name for u in obj.users]
-    result["permissions"] = [p.permission_name for p in obj.permissions]
+    result['users'] = [u.user_name for u in obj.users]
+    result['permissions'] = [p.permission_name for p in obj.permissions]
     return result
 
 @jsonify.when('isinstance(obj, User)')
@@ -43,14 +43,12 @@
     #elif $identity == "sqlalchemy"
     result = jsonify_saobject(obj)
     #end if
-    try: del result['password']
-    except KeyError: pass
+    result.pop('password', None)
     #if $identity == "sqlalchemy"
-    try: del result['_password']
-    except KeyError: pass
+    result.pop('_password', None)
     #end if
-    result["groups"] = [g.group_name for g in obj.groups]
-    result["permissions"] = [p.permission_name for p in obj.permissions]
+    result['groups'] = [g.group_name for g in obj.groups]
+    result['permissions'] = [p.permission_name for p in obj.permissions]
     return result
 
 @jsonify.when('isinstance(obj, Permission)')
@@ -60,6 +58,6 @@
     #elif $identity == "sqlalchemy"
     result = jsonify_saobject(obj)
     #end if
-    result["groups"] = [g.group_name for g in obj.groups]
+    result['groups'] = [g.group_name for g in obj.groups]
     return result
 #end if

Modified: branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl
==============================================================================
--- branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl      
Mon Jan 17 03:33:27 2011        (r7204)
+++ branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl      
Mon Jan 17 06:36:05 2011        (r7205)
@@ -130,7 +130,7 @@
 
 
 class Group(SQLObject):
-    """An ultra-simple group definition."""
+    """An ultra-simple Group definition."""
 
     # names like "user" and "group" are reserved words in SQL
     # so we set the name to something safe for SQL
@@ -258,7 +258,7 @@
 
 
 class Group(Entity):
-    """An ultra-simple group definition."""
+    """An ultra-simple Group definition."""
 
     using_options(tablename='tg_group')
 
@@ -439,7 +439,7 @@
 
 
 class Group(object):
-    """An ultra-simple group definition."""
+    """An ultra-simple Group definition."""
 
     def __repr__(self):
         return '<Group: name="%s", display_name="%s">' % (

Modified: branches/1.5/turbogears/visit/savisit.py
==============================================================================
--- branches/1.5/turbogears/visit/savisit.py    Mon Jan 17 03:33:27 2011        
(r7204)
+++ branches/1.5/turbogears/visit/savisit.py    Mon Jan 17 06:36:05 2011        
(r7205)
@@ -31,16 +31,13 @@
             log.error(msg)
         else:
             log.info("Successfully loaded '%s'", visit_class_path)
-
         if visit_class is TG_Visit:
-            # Handle it gracefully when TG_Visit is already mapped.
-            # May happen, when the visit manager is shutdown and started again
             try:
                 class_mapper(visit_class)
             except UnmappedClassError:
-                mapper(visit_class, visits_table)
-        # base-class' __init__ triggers self.create_model, so mappers need to
-        # be initialized before.
+                visit_class._map()
+        # base-class' __init__ triggers self.create_model,
+        # so mappers need to be initialized before.
         super(SqlAlchemyVisitManager, self).__init__(timeout)
 
     def create_model(self):
@@ -91,17 +88,18 @@
                 values=dict(expiry=expiry)))
 
 
-# The Visit table
-
-visits_table = Table('tg_visit', metadata,
-    Column('visit_key', String(40), primary_key=True),
-    Column('created', DateTime, nullable=False, default=datetime.now),
-    Column('expiry', DateTime)
-)
-
+# The default Visit model class
 
 class TG_Visit(object):
 
     @classmethod
     def lookup_visit(cls, visit_key):
         return session.query(cls).get(visit_key)
+
+    @classmethod
+    def _map(cls):
+        cls._table = Table('visit', metadata,
+            Column('visit_key', String(40), primary_key=True),
+            Column('created', DateTime, nullable=False, default=datetime.now),
+            Column('expiry', DateTime))
+        cls._mapper = mapper(cls, cls._table)

Reply via email to