diff --git a/ordr2/models/__init__.py b/ordr2/models/__init__.py index 9100be1..1a507c8 100644 --- a/ordr2/models/__init__.py +++ b/ordr2/models/__init__.py @@ -11,7 +11,7 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import MyModel # flake8: noqa +from .users import User, Role # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/ordr2/models/mymodel.py b/ordr2/models/mymodel.py deleted file mode 100644 index 8c9d701..0000000 --- a/ordr2/models/mymodel.py +++ /dev/null @@ -1,18 +0,0 @@ -from sqlalchemy import ( - Column, - Index, - Integer, - Text, - ) - -from .meta import Base - - -class MyModel(Base): - __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Text) - value = Column(Integer) - - -Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/ordr2/models/users.py b/ordr2/models/users.py new file mode 100644 index 0000000..0f93f06 --- /dev/null +++ b/ordr2/models/users.py @@ -0,0 +1,92 @@ +''' User Account and Roles Models ''' + +import enum + +from datetime import datetime +from sqlalchemy import ( + Column, + Date, + Enum, + Integer, + Text, + ) + +from .meta import Base + + +class Role(enum.Enum): + ''' roles of user accounts ''' + + #: new user, email address was not validated + UNVALIDATED = 'unvalidated' + + #: new user, email address validated, not activated by admin + NEW = 'new' + + #: standard user of the system, can place and view orders + USER = 'user' + + #: privileged user of the system, can edit orders + PURCHASER = 'purchaser' + + #: privileged user, can edit orders, users and consumables + ADMIN = 'admin' + + #: a user that is no longer activated + INACTIVE = 'inactive' + + @property + def principal(self): + ''' returns the principal identifier of the role ''' + return 'role:' + self.value.lower() + + def __str__(self): + ''' string representation ''' + return self.value.capitalize() + + +class User(Base): + ''' A user of the application ''' + + __tablename__ = 'users' + + #: primary key + id = Column(Integer, primary_key=True) + #: unique user name + username = Column(Text, nullable=False, unique=True) + #: hashed password, see ``ordr2.security`` + password_hash = Column(Text, nullable=False) + #: role of the user, see ``ordr2.models.users.Role`` + role = Column(Enum(Role), nullable=False) + + first_name = Column(Text, nullable=False) + last_name = Column(Text, nullable=False) + email = Column(Text, nullable=False, unique=True) + date_created = Column(Date, nullable=False, default=datetime.utcnow) + + @property + def principal(self): + ''' returns the principal identifier for the user ''' + return 'user:{}'.format(self.id) + + @property + def role_principals(self): + ''' returns the principal identifiers for the user's role ''' + principals = [self.role.principal] + if self.role is Role.PURCHASER: + # a purchaser is also a user + principals.append(Role.USER.principal) + elif self.role is Role.ADMIN: + # an admin is also a purchaser and a user + principals.append(Role.PURCHASER.principal) + principals.append(Role.USER.principal) + return principals + + @property + def is_active(self): + ''' is true if the user has an active role ''' + return self.role in (Role.USER, Role.PURCHASER, Role.ADMIN) + + def __str__(self): + ''' string representation ''' + return str(self.username) diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..147eb15 --- /dev/null +++ b/tests/models/__init__.py @@ -0,0 +1 @@ +''' Test package for ordr2.models ''' diff --git a/tests/models/users.py b/tests/models/users.py new file mode 100644 index 0000000..c86d931 --- /dev/null +++ b/tests/models/users.py @@ -0,0 +1,89 @@ +''' Test package for ordr2.models.users ''' + +import pytest + + +# tests for users.Role + +def test_role_principals(): + ''' test Role.principal, a caluclated property ''' + from ordr2.models.users import Role + + assert Role.UNVALIDATED.principal == 'role:unvalidated' + assert Role.NEW.principal == 'role:new' + assert Role.USER.principal == 'role:user' + assert Role.PURCHASER.principal == 'role:purchaser' + assert Role.ADMIN.principal == 'role:admin' + assert Role.INACTIVE.principal == 'role:inactive' + + +def test_role_str(): + ''' test the string representation of roles ''' + from ordr2.models.users import Role + + assert str(Role.UNVALIDATED) == 'Unvalidated' + assert str(Role.NEW) == 'New' + assert str(Role.USER) == 'User' + assert str(Role.PURCHASER) == 'Purchaser' + assert str(Role.ADMIN) == 'Admin' + assert str(Role.INACTIVE) == 'Inactive' + + +# tests for users.User + +def test_user_principal(): + ''' test the user principal calculated property ''' + from ordr2.models.users import User + + user = User(id=3) + + assert user.principal == 'user:3' + + +@pytest.mark.parametrize( + 'role_name, principals', [ + ('UNVALIDATED', ['role:unvalidated']), + ('NEW', ['role:new']), + ('USER', ['role:user']), + ('PURCHASER', ['role:purchaser', 'role:user']), + ('ADMIN', ['role:admin', 'role:purchaser', 'role:user']), + ('INACTIVE', ['role:inactive']) + ] + ) +def test_user_role_principals(role_name, principals): + ''' test the user's role principals calculated property ''' + from ordr2.models.users import User, Role + + role = Role[role_name] + user = User(role=role) + + assert user.role_principals == principals + + +@pytest.mark.parametrize( + 'role_name, is_active', [ + ('UNVALIDATED', False), + ('NEW', False), + ('USER', True), + ('PURCHASER', True), + ('ADMIN', True), + ('INACTIVE', False) + ] + ) +def test_user_is_active(role_name, is_active): + ''' test if is_active returns correct value based on the user's role ''' + from ordr2.models.users import User, Role + + role = Role[role_name] + user = User(role=role) + + assert user.is_active == is_active + + +def test_user_string_representation(): + ''' test the string representation of the user ''' + from ordr2.models.users import User, Role + + user = User(username='FooBar') + + assert str(user) == 'FooBar'