Add an sqlalchemy model that works with the SQL Schema.

This commit is contained in:
Toshio Kuratomi 2008-02-16 12:10:04 -08:00
parent fc9e791974
commit 6581d5a395
2 changed files with 322 additions and 23 deletions

View file

@ -1,3 +1,28 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2007-2008 Red Hat, Inc. All rights reserved.
#
# This copyrighted material is made available to anyone wishing to use, modify,
# copy, or redistribute it subject to the terms and conditions of the GNU
# General Public License v.2. This program is distributed in the hope that it
# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details. You should have
# received a copy of the GNU General Public License along with this program;
# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
# incorporated in the source code or documentation are not subject to the GNU
# General Public License and may only be used or replicated with the express
# permission of Red Hat, Inc.
#
# Red Hat Author(s): Toshio Kuratomi <tkuratom@redhat.com>
#
'''
JSON Helper functions. Most JSON code directly related to classes is
implemented via the __json__() methods in model.py. These methods define
methods of transforming a class into json for a few common types.
'''
# A JSON-based API(view) for your app. # A JSON-based API(view) for your app.
# Most rules would look like: # Most rules would look like:
# @jsonify.when("isinstance(obj, YourClass)") # @jsonify.when("isinstance(obj, YourClass)")
@ -6,5 +31,91 @@
# @jsonify can convert your objects to following types: # @jsonify can convert your objects to following types:
# lists, dicts, numbers and strings # lists, dicts, numbers and strings
import sqlalchemy
from turbojson.jsonify import jsonify from turbojson.jsonify import jsonify
class SABase(object):
'''Base class for SQLAlchemy mapped objects.
This base class makes sure we have a __json__() method on each SQLAlchemy
mapped object that knows how to::
1) return json for the object.
2) Can selectively add tables pulled in from the table to the data we're
returning.
'''
# pylint: disable-msg=R0903
def __json__(self):
'''Transform any SA mapped class into json.
This method takes an SA mapped class and turns the "normal" python
attributes into json. The properties (from properties in the mapper)
are also included if they have an entry in jsonProps. You make
use of this by setting jsonProps in the controller.
Example controller::
john = model.Person.get_by(name='John')
# Person has a property, addresses, linking it to an Address class.
# Address has a property, phone_nums, linking it to a Phone class.
john.jsonProps = {'Person': ['addresses'],
'Address': ['phone_nums']}
return dict(person=john)
jsonProps is a dict that maps class names to lists of properties you
want to output. This allows you to selectively pick properties you
are interested in for one class but not another. You are responsible
for avoiding loops. ie: *don't* do this::
john.jsonProps = {'Person': ['addresses'], 'Address': ['people']}
'''
props = {}
# pylint: disable-msg=E1101
if 'jsonProps' in self.__dict__ \
and self.jsonProps.has_key(self.__class__.__name__):
propList = self.jsonProps[self.__class__.__name__]
else:
propList = {}
# pylint: enable-msg=E1101
# Load all the columns from the table
for key in self.mapper.props.keys(): # pylint: disable-msg=E1101
if isinstance(self.mapper.props[key], # pylint: disable-msg=E1101
sqlalchemy.orm.properties.ColumnProperty):
props[key] = getattr(self, key)
# Load things that are explicitly listed
for field in propList:
props[field] = getattr(self, field)
try:
# pylint: disable-msg=E1101
props[field].jsonProps = self.jsonProps
except AttributeError: # pylint: disable-msg=W0704
# Certain types of objects are terminal and won't allow setting
# jsonProps
pass
return props
@jsonify.when("isinstance(obj, sqlalchemy.orm.query.Query)" \
" or isinstance(obj, sqlalchemy.ext.selectresults.SelectResults)")
def jsonify_sa_select_results(obj):
'''Transform selectresults into lists.
The one special thing is that we bind the special jsonProps into each
descendent. This allows us to specify a jsonProps on the toplevel
query result and it will pass to all of its children.
'''
if 'jsonProps' in obj.__dict__:
for element in obj:
element.jsonProps = obj.jsonProps
return list(obj)
@jsonify.when("isinstance(obj, sqlalchemy.orm.attributes.InstrumentedList)")
def jsonify_salist(obj):
'''Transform SQLAlchemy InstrumentedLists into json.
The one special thing is that we bind the special jsonProps into each
descendent. This allows us to specify a jsonProps on the toplevel
query result and it will pass to all of its children.
'''
if 'jsonProps' in obj.__dict__:
for element in obj:
element.jsonProps = obj.jsonProps
return [jsonify(element) for element in obj]

View file

@ -1,33 +1,221 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2008 Red Hat, Inc. All rights reserved.
#
# This copyrighted material is made available to anyone wishing to use, modify,
# copy, or redistribute it subject to the terms and conditions of the GNU
# General Public License v.2. This program is distributed in the hope that it
# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details. You should have
# received a copy of the GNU General Public License along with this program;
# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
# incorporated in the source code or documentation are not subject to the GNU
# General Public License and may only be used or replicated with the express
# permission of Red Hat, Inc.
#
# Author(s): Toshio Kuratomi <tkuratom@redhat.com>
#
'''
Model for the
'''
from datetime import datetime from datetime import datetime
from turbogears.database import PackageHub from turbogears.database import metadata, mapper
from sqlobject import * # import some basic SQLAlchemy classes for declaring the data model
from turbogears import identity # (see http://www.sqlalchemy.org/docs/04/ormtutorial.html)
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.orm import relation
# import some datatypes for table columns from SQLAlchemy
# (see http://www.sqlalchemy.org/docs/04/types.html for more)
from sqlalchemy import String, Unicode, Integer, DateTime
# A few sqlalchemy tricks:
# Allow viewing foreign key relations as a dictionary
from sqlalchemy.orm.collections import column_mapped_collection
# Allow us to reference the remote table of a many:many as a simple list
from sqlalchemy.ext.associationproxy import association_proxy
hub = PackageHub("fas") from turbogears import identity
__connection__ = hub
# class YourDataClass(SQLObject): from fas.json import SABase
# pass # Soon we'll use this instead:
#from fedora.tg.json import SABase
# identity models. #
class Visit(SQLObject): # Tables Mapped from the DB
class sqlmeta: #
table = "visit"
visit_key = StringCol(length=40, alternateID=True, PeopleTable = Table('people', metadata, autoload=True)
alternateMethodName="by_visit_key") PersonEmailsTable = Table('person_emails', metadata, autoload=True)
created = DateTimeCol(default=datetime.now) PersonRolesTable = Table('person_roles', metadata, autoload=True)
expiry = DateTimeCol() ConfigsTable = Table('configs', metadata, autoload=True)
GroupsTable = Table('groups', metadata, autoload=True)
GroupEmailsTable = Table('group_emails', metadata, autoload=True)
GroupRolesTable = Table('group_roles', metadata, autoload=True)
BugzillaQueueTable = Table('bugzilla_queue', metadata, autoload=True)
# The identity schema -- These must follow some conventions that TG
# understands and are shared with other Fedora services via the python-fedora
# module.
visits_table = Table('visit', metadata,
Column('visit_key', String(40), primary_key=True),
Column('created', DateTime, nullable=False, default=datetime.now),
Column('expiry', DateTime)
)
visit_identity_table = Table('visit_identity', metadata,
Column('visit_key', String(40), ForeignKey('visit.visit_key'),
primary_key=True),
Column('user_id', Integer, ForeignKey('people.id'), index=True)
)
#
# Mapped Classes
#
class People(SABase):
'''Records for all the contributors to Fedora.'''
pass
memberships = association_proxy('roles', 'group')
# It's possible we want to merge this into the People class
'''
class User(object):
"""
Reasonably basic User definition.
Probably would want additional attributes.
"""
def permissions(self):
perms = set()
for g in self.groups:
perms |= set(g.permissions)
return perms
permissions = property(permissions)
def by_email_address(cls, email):
"""
A class method that can be used to search users
based on their email addresses since it is unique.
"""
return cls.query.filter_by(email_address=email).first()
by_email_address = classmethod(by_email_address)
def by_user_name(cls, username):
"""
A class method that permits to search users
based on their user_name attribute.
"""
return cls.query.filter_by(user_name=username).first()
by_user_name = classmethod(by_user_name)
def _set_password(self, password):
"""
encrypts password on the fly using the encryption
algo defined in the configuration
"""
self._password = identity.encrypt_password(password)
def _get_password(self):
"""
returns password
"""
return self._password
password = property(_get_password, _set_password)
'''
class PersonEmails(SABase):
'''Map a person to an email address.'''
pass
class PersonRoles(SABase):
'''Record people that are members of groups.'''
pass
class Configs(SABase):
'''Configs for applications that a Fedora Contributor uses.'''
pass
class Groups(SABase):
'''Group that people can belong to.'''
pass
# People in the group
people = association_proxy('roles', 'member')
# Groups in the group
groups = association_proxy('group_members', 'member')
# Groups that this group belongs to
memberships = association_proxy('group_roles', 'group')
class GroupEmails(SABase):
'''Map a group to an email address.'''
pass
class GroupRoles(SABase):
'''Record groups that are members of other groups.'''
pass
class BugzillaQueue(SABase):
'''Queued up changes that need to be applied to bugzilla.'''
pass
class Visit(SABase):
'''Track how many people are visiting the website.
It doesn't currently make sense for us to track this here so we clear this
table of stale records every hour.
'''
def lookup_visit(cls, visit_key): def lookup_visit(cls, visit_key):
try: return cls.query.get(visit_key)
return cls.by_visit_key(visit_key)
except SQLObjectNotFound:
return None
lookup_visit = classmethod(lookup_visit) lookup_visit = classmethod(lookup_visit)
class VisitIdentity(SQLObject):
visit_key = StringCol(length=40, alternateID=True,
alternateMethodName="by_visit_key")
user_id = IntCol()
class VisitIdentity(SABase):
'''Associate a user with a visit cookie.
This allows users to log in to app.
'''
pass
#
# set up mappers between tables and classes
#
mapper(People, PeopleTable)
mapper(PersonEmails, PersonEmailsTable, properties = {
person: relation(People, backref = 'emails',
collection_class = column_mapped_collection(
PersonEmailsTable.c.purpose)
})
mapper(PersonRoles, PersonRolesTable, properties = {
member: relation(People, backref = 'roles'),
group: relation(Groups, backref='roles')
})
mapper(Configs, ConfigsTable, properties = {
person: relation(People, backref = 'configs'
})
mapper(Groups, GroupsTable)
mapper(GroupEmails, GroupEmailsTable, properties = {
group: relation(Group, backref = 'emails',
collection_class = column_mapped_collection(
GroupEmailsTable.c.purpose)
})
# GroupRoles are complex because the group is a member of a group and thus
# is referencing the same table.
mapper(GroupRoles, GroupRolesTable, properties = {
member: relation(Groups, backref = 'group_roles',
primaryjoin = GroupsTable.c.id==GroupRolesTable.c.member_id),
group: relation(Groups, backref = 'group_members',
primaryjoin = GroupsTable.c.id==GroupRolesTable.c.group_id)
})
mapper(BugzillaQueue, BugzillaQueueTable, properties = {
group: relation(Groups, backref = 'pending'),
person: relation(People, backref = 'pending')
})
# TurboGears Identity
mapper(Visit, visits_table)
mapper(VisitIdentity, visit_identity_table,
properties=dict(users=relation(People, backref='visit_identity')))