From 0c6601576f99b2c3c964f66890b44c648d4b3945 Mon Sep 17 00:00:00 2001 From: tanshu Date: Tue, 9 Jun 2015 17:04:58 +0530 Subject: [PATCH] Still very initial scaffolding --- development.ini | 6 +- setup.py | 5 +- soter/__init__.py | 6 +- soter/models/__init__.py | 52 ++++++++++++++ soter/models/auth.py | 149 +++++++++++++++++++++++++++++++++++++++ soter/models/guidtype.py | 56 +++++++++++++++ soter/models/master.py | 135 +++++++++++++++++++++++++++++++++++ 7 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 soter/models/__init__.py create mode 100644 soter/models/auth.py create mode 100644 soter/models/guidtype.py create mode 100644 soter/models/master.py diff --git a/development.ini b/development.ini index fb6a7e0..6275eab 100644 --- a/development.ini +++ b/development.ini @@ -11,12 +11,8 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_debugtoolbar -# By default, the toolbar only appears for clients from IP addresses -# '127.0.0.1' and '::1'. -# debugtoolbar.hosts = 127.0.0.1 ::1 +sqlalchemy.url = postgresql://postgres:123456@localhost:5432/pics ### # wsgi server configuration diff --git a/setup.py b/setup.py index 963c2e6..ebd6db7 100644 --- a/setup.py +++ b/setup.py @@ -10,9 +10,10 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', - 'pyramid_debugtoolbar', 'waitress', + 'transaction', + 'zope.sqlalchemy', + 'SQLAlchemy', ] setup(name='Soter', diff --git a/soter/__init__.py b/soter/__init__.py index 7a20df8..4eac825 100644 --- a/soter/__init__.py +++ b/soter/__init__.py @@ -1,11 +1,15 @@ from pyramid.config import Configurator +from sqlalchemy import engine_from_config +from soter.models import initialize_sql def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ + engine = engine_from_config(settings, 'sqlalchemy.') + initialize_sql(engine) + config = Configurator(settings=settings) - config.include('pyramid_chameleon') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') diff --git a/soter/models/__init__.py b/soter/models/__init__.py new file mode 100644 index 0000000..48c026a --- /dev/null +++ b/soter/models/__init__.py @@ -0,0 +1,52 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker +from zope.sqlalchemy import ZopeTransactionExtension + + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension(), expire_on_commit=False)) +Base = declarative_base() + + +def initialize_sql(engine): + DBSession.configure(bind=engine) + Base.metadata.bind = engine + if not schema_exists(engine): + fixtures(engine) + + +def schema_exists(engine): + from soter.models.master import DbSetting + + with engine.connect() as connection: + return engine.dialect.has_table(connection, DbSetting.__tablename__) + + +def fixtures(engine): + import transaction + import uuid + from soter.models.master import Album, DbSetting, Picture, Tag, picture_tag + from soter.models.auth import Permission, Role, User, role_permission, role_user + + Base.metadata.create_all(engine) + + user = User('Admin', 'Administrator', 'soter@tanshu.com', '123456', False, + uuid.UUID('8de98592-76d9-c74d-bb3f-d6184d388b5a')) + DBSession.add(user) + + roles = [Role('Owner', uuid.UUID('52e08c0c-048a-784f-be10-6e129ad4b5d4'))] + + for role in roles: + DBSession.add(role) + user.roles.append(role) + + permissions = [Permission('Attendance', uuid.UUID('09d05434-a09a-fa45-963b-769a2e3fc667')), + Permission('Trial Balance', uuid.UUID('3b099fec-ddc5-4243-b30e-afb78d9ca14a')), + Permission('Cash Flow', uuid.UUID('c4d3ae29-420b-ea4c-ae90-00a356263fd9')), + Permission('Cost Centers', uuid.UUID('6fcc1a20-6aec-e840-b334-1632b34aeab8'))] + + for permission in permissions: + DBSession.add(permission) + roles[0].permissions.append(permission) + + transaction.commit() + diff --git a/soter/models/auth.py b/soter/models/auth.py new file mode 100644 index 0000000..b6abbe1 --- /dev/null +++ b/soter/models/auth.py @@ -0,0 +1,149 @@ +from hashlib import sha256 +import uuid +from sqlalchemy import Column, Unicode, Boolean, Table, ForeignKey, UniqueConstraint +from sqlalchemy.orm import relationship, synonym +from soter.models import Base, DBSession +from soter.models.guidtype import GUID + + +def encrypt(val): + return sha256(val.encode('utf-8') + "Salt".encode('utf-8')).hexdigest() + + +role_user = Table( + 'role_users', Base.metadata, + Column('id', GUID(), primary_key=True, default=uuid.uuid4), + Column('user_id', GUID(), ForeignKey('users.id')), + Column('role_id', GUID(), ForeignKey('roles.id')), + UniqueConstraint('user_id', 'role_id') +) + +role_permission = Table( + 'role_permissions', Base.metadata, + Column('id', GUID(), primary_key=True, default=uuid.uuid4), + Column('permission_id', GUID(), ForeignKey('permissions.id')), + Column('role_id', GUID(), ForeignKey('roles.id')), + UniqueConstraint('permission_id', 'role_id') +) + + +class User(Base): + __tablename__ = 'users' + + id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) + name = Column('name', Unicode(255), unique=True) + display_name = Column('display_name', Unicode(255)) + email = Column('email', Unicode(255), unique=True) + _password = Column('password', Unicode(64)) + locked_out = Column('locked_out', Boolean) + + roles = relationship("Role", secondary=role_user) + + def _get_password(self): + return self._password + + def _set_password(self, password): + self._password = encrypt(password) + + password = property(_get_password, _set_password) + password = synonym('_password', descriptor=password) + + + @property + def __name__(self): + return self.name + + def __init__(self, name=None, display_name=None, email=None, password=None, locked_out=None, id=None): + self.name = name + self.display_name = display_name + self.email = email + self.password = password + self.locked_out = locked_out + self.id = id + + @classmethod + def by_name(cls, name): + if not name: + return None + return DBSession.query(cls).filter(cls.name.ilike(name)).first() + + @classmethod + def by_email(cls, email): + if not email: + return None + return DBSession.query(cls).filter(cls.email.ilike(email)).first() + + @classmethod + def by_id(cls, id): + if not isinstance(id, uuid.UUID): + id = uuid.UUID(id) + return DBSession.query(cls).filter(cls.id == id).one() + + @classmethod + def auth(cls, name, password): + if password is None: + return False, None + user = cls.by_name(name) + if not user: + return False, None + if user.password != encrypt(password) or user.locked_out: + return False, None + else: + return True, user + + @classmethod + def list(cls): + return DBSession.query(cls).order_by(cls.name).all() + + @classmethod + def query(cls): + return DBSession.query(cls) + + @classmethod + def filtered_list(cls, name): + query = DBSession.query(cls) + for item in name.split(): + query = query.filter(cls.name.ilike('%' + item + '%')) + return query.order_by(cls.name) + + +class Role(Base): + __tablename__ = 'roles' + + id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) + name = Column('name', Unicode(255), unique=True) + + + def __init__(self, name=None, id=None): + self.name = name + self.id = id + + @classmethod + def by_id(cls, id): + return DBSession.query(cls).filter(cls.id == id).one() + + @classmethod + def list(cls): + return DBSession.query(cls).order_by(cls.name).all() + + +class Permission(Base): + __tablename__ = 'permissions' + + id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) + name = Column('name', Unicode(255), unique=True) + + roles = relationship("Role", secondary=role_permission, backref="permissions") + + def __init__(self, name=None, id=None): + self.name = name + self.id = id + + @classmethod + def list(cls): + return DBSession.query(cls).order_by(cls.name).all() + + @classmethod + def by_id(cls, id): + return DBSession.query(cls).filter(cls.id == id).one() + diff --git a/soter/models/guidtype.py b/soter/models/guidtype.py new file mode 100644 index 0000000..cdc2e8a --- /dev/null +++ b/soter/models/guidtype.py @@ -0,0 +1,56 @@ +from sqlalchemy.types import TypeDecorator, CHAR, Binary +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.sqlite import BLOB +import uuid + +class GUID(TypeDecorator): + """Platform-independent GUID type. + + Uses Postgresql's UUID type, otherwise uses + CHAR(32), storing as stringified hex values. + + """ + impl = Binary + + # if dialect.value == 'postgresql': + # impl = CHAR + # elif dialect.value == 'mysql': + # impl = MSBinary + # elif dialect.valie == 'sqlite': + # impl = Binary + # else: + # impl = Binary + + def load_dialect_impl(self, dialect): + if dialect.name == 'postgresql': + return dialect.type_descriptor(UUID()) + elif dialect.name == 'sqlite': + return dialect.type_descriptor(BLOB()) + else: + return dialect.type_descriptor(CHAR(32)) + + def process_bind_param(self, value, dialect): + if value is None: + return None + elif dialect.name == 'postgresql': + return str(value) + elif not isinstance(value,uuid.UUID): + raise ValueError('value %s is not a valid uuid.UUID' % value) + else: + return value.bytes + # if not isinstance(value, uuid.UUID): + # return "%.32x" % uuid.UUID(value) + # else: + # # hexstring + # return "%.32x" % value + + def process_result_value(self,value,dialect=None): + if value is None: + return None + elif isinstance(value, bytes): + return uuid.UUID(bytes=value) + else: + return uuid.UUID(value) + + def is_mutable(self): + return False \ No newline at end of file diff --git a/soter/models/master.py b/soter/models/master.py new file mode 100644 index 0000000..aea3e18 --- /dev/null +++ b/soter/models/master.py @@ -0,0 +1,135 @@ +import uuid +from sqlalchemy import Column, PickleType, Unicode, ForeignKey, Boolean, Table, UniqueConstraint +from sqlalchemy.orm import relationship +from soter.models import Base, DBSession +from soter.models.guidtype import GUID + + +class DbSetting(Base): + __tablename__ = 'settings' + + id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) + name = Column('name', Unicode(255), unique=True, nullable=False) + data = Column('data', PickleType) + + def __init__(self, id=None, name=None, data=None): + self.id = id + self.name = name + self.data = data + + @classmethod + def by_id(cls, id): + if not isinstance(id, uuid.UUID): + id = uuid.UUID(id) + return DBSession.query(cls).filter(cls.id == id).first() + + @classmethod + def by_name(cls, name): + return DBSession.query(cls).filter(cls.name == name).first() + + @classmethod + def list(cls): + return DBSession.query(cls).order_by(cls.name).all() + + +picture_tag = Table( + 'picture_tags', Base.metadata, + Column('id', GUID(), primary_key=True, default=uuid.uuid4), + Column('picture_id', GUID(), ForeignKey('pictures.id'), nullable=False), + Column('tag_id', GUID(), ForeignKey('tags.id'), nullable=False), + UniqueConstraint('picture_id', 'tag_id') +) + +class Album(Base): + __tablename__ = 'albums' + + id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) + name = Column('name', Unicode(255), unique=True) + user_id = Column('user_id', GUID(), ForeignKey('users.id'), nullable=False) + is_fixture = Column('is_fixture', Boolean, nullable=False) + is_public = Column('is_public', Boolean, nullable=False) + + user = relationship('User', primaryjoin="User.id==Album.user_id", cascade=None) + + pictures = relationship('Picture', backref='album') + + def __init__(self, name, user_id, is_public, id=None, is_fixture=False): + self.name = name + self.user_id = user_id + self.is_public = is_public + self.id = id + self.is_fixture = is_fixture + + @classmethod + def list(cls): + return DBSession.query(cls).order_by(cls.name) + + @classmethod + def by_id(cls, id): + return DBSession.query(cls).filter(cls.id == id).first() + + @classmethod + def menu_item(cls): + return uuid.UUID('dad46805-f577-4e5b-8073-9b788e0173fc') + + @classmethod + def semi(cls): + return uuid.UUID('e6bf81b9-1e9b-499f-81d5-ab5662e9d9b1') + + +class Picture(Base): + __tablename__ = 'pictures' + + id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) + name = Column('name', Unicode(255), unique=True) + user_id = Column('user_id', GUID(), ForeignKey('users.id'), nullable=False) + album_id = Column('album_id', GUID(), ForeignKey('albums.id'), nullable=False) + is_public = Column('is_public', Boolean, nullable=False) + + user = relationship('User', primaryjoin="User.id==Picture.user_id", cascade=None) + + def __init__(self, name, user_id, is_public, id=None, is_fixture=False): + self.name = name + self.user_id = user_id + self.is_public = is_public + self.id = id + self.is_fixture = is_fixture + + @classmethod + def list(cls): + return DBSession.query(cls).order_by(cls.name) + + @classmethod + def by_id(cls, id): + return DBSession.query(cls).filter(cls.id == id).first() + + @classmethod + def menu_item(cls): + return uuid.UUID('dad46805-f577-4e5b-8073-9b788e0173fc') + + @classmethod + def semi(cls): + return uuid.UUID('e6bf81b9-1e9b-499f-81d5-ab5662e9d9b1') + + +class Tag(Base): + __tablename__ = 'tags' + + id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) + name = Column('name', Unicode(100), unique=True, nullable=False) + + def __init__(self, name=None): + self.name = name + + @classmethod + def by_id(cls, id): + return DBSession.query(cls).filter(cls.id == id).first() + + @classmethod + def by_name(cls, name): + return DBSession.query(cls).filter(cls.name == name).first() + + @classmethod + def list(cls): + return DBSession.query(cls).all() +