diff --git a/fas/fas/json.py b/fas/fas/json.py index 66d5cfb..85c5ed9 100644 --- a/fas/fas/json.py +++ b/fas/fas/json.py @@ -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 +# + +''' +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. # Most rules would look like: # @jsonify.when("isinstance(obj, YourClass)") @@ -6,5 +31,91 @@ # @jsonify can convert your objects to following types: # lists, dicts, numbers and strings +import sqlalchemy 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] diff --git a/fas/fas/model.py b/fas/fas/model.py index c0407f1..91926f5 100644 --- a/fas/fas/model.py +++ b/fas/fas/model.py @@ -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 +# + +''' +Model for the +''' from datetime import datetime -from turbogears.database import PackageHub -from sqlobject import * -from turbogears import identity +from turbogears.database import metadata, mapper +# import some basic SQLAlchemy classes for declaring the data model +# (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") -__connection__ = hub +from turbogears import identity -# class YourDataClass(SQLObject): -# pass +from fas.json import SABase +# Soon we'll use this instead: +#from fedora.tg.json import SABase -# identity models. -class Visit(SQLObject): - class sqlmeta: - table = "visit" +# +# Tables Mapped from the DB +# - visit_key = StringCol(length=40, alternateID=True, - alternateMethodName="by_visit_key") - created = DateTimeCol(default=datetime.now) - expiry = DateTimeCol() +PeopleTable = Table('people', metadata, autoload=True) +PersonEmailsTable = Table('person_emails', metadata, autoload=True) +PersonRolesTable = Table('person_roles', metadata, autoload=True) +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): - try: - return cls.by_visit_key(visit_key) - except SQLObjectNotFound: - return None + return cls.query.get(visit_key) 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')))