diff --git a/src/wuttjamaican/db/alembic/versions/3abcc44f7f91_add_people.py b/src/wuttjamaican/db/alembic/versions/3abcc44f7f91_add_people.py new file mode 100644 index 0000000..143f6df --- /dev/null +++ b/src/wuttjamaican/db/alembic/versions/3abcc44f7f91_add_people.py @@ -0,0 +1,45 @@ +"""add people + +Revision ID: 3abcc44f7f91 +Revises: d686f7abe3e0 +Create Date: 2024-07-14 15:14:30.552682 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '3abcc44f7f91' +down_revision: Union[str, None] = 'd686f7abe3e0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # person + op.create_table('person', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('full_name', sa.String(length=100), nullable=False), + sa.Column('first_name', sa.String(length=50), nullable=True), + sa.Column('middle_name', sa.String(length=50), nullable=True), + sa.Column('last_name', sa.String(length=50), nullable=True), + sa.PrimaryKeyConstraint('uuid', name=op.f('pk_person')) + ) + + # user + op.add_column('user', sa.Column('person_uuid', sa.String(length=32), nullable=True)) + op.create_foreign_key(op.f('fk_user_person_uuid_person'), 'user', 'person', ['person_uuid'], ['uuid']) + + +def downgrade() -> None: + + # user + op.drop_constraint(op.f('fk_user_person_uuid_person'), 'user', type_='foreignkey') + op.drop_column('user', 'person_uuid') + + # person + op.drop_table('person') diff --git a/src/wuttjamaican/db/model/__init__.py b/src/wuttjamaican/db/model/__init__.py index 66b36e5..760e3a6 100644 --- a/src/wuttjamaican/db/model/__init__.py +++ b/src/wuttjamaican/db/model/__init__.py @@ -31,11 +31,12 @@ The ``wuttjamaican.db.model`` namespace contains the following: * :func:`~wuttjamaican.db.model.base.uuid_fk_column()` * :class:`~wuttjamaican.db.model.base.Base` * :class:`~wuttjamaican.db.model.base.Setting` +* :class:`~wuttjamaican.db.model.base.Person` * :class:`~wuttjamaican.db.model.auth.Role` * :class:`~wuttjamaican.db.model.auth.Permission` * :class:`~wuttjamaican.db.model.auth.User` * :class:`~wuttjamaican.db.model.auth.UserRole` """ -from .base import Base, Setting, uuid_column, uuid_fk_column +from .base import uuid_column, uuid_fk_column, Base, Setting, Person from .auth import Role, Permission, User, UserRole diff --git a/src/wuttjamaican/db/model/auth.py b/src/wuttjamaican/db/model/auth.py index 57360b3..9a6c1ac 100644 --- a/src/wuttjamaican/db/model/auth.py +++ b/src/wuttjamaican/db/model/auth.py @@ -166,6 +166,17 @@ class User(Base): Hashed password for login. (The raw password is not stored.) """) + person_uuid = uuid_fk_column('person.uuid', nullable=True) + person = orm.relationship( + 'Person', + # TODO: seems like this is not needed? + # uselist=False, + back_populates='users', + doc=""" + Reference to the :class:`~wuttjamaican.db.model.base.Person` + whose user account this is. + """) + active = sa.Column(sa.Boolean(), nullable=False, default=True, doc=""" Flag indicating whether the user account is "active" - it is ``True`` by default. @@ -192,6 +203,10 @@ class User(Base): ) def __str__(self): + if self.person: + name = str(self.person) + if name: + return name return self.username or "" diff --git a/src/wuttjamaican/db/model/base.py b/src/wuttjamaican/db/model/base.py index d596e8f..42b16e1 100644 --- a/src/wuttjamaican/db/model/base.py +++ b/src/wuttjamaican/db/model/base.py @@ -85,3 +85,78 @@ class Setting(Base): def __str__(self): return self.name or "" + + +class Person(Base): + """ + Represents a person. + + The use for this table in the base framework, is to associate with + a :class:`~wuttjamaican.db.model.auth.User` to provide first and + last name etc. (However a user does not have to be associated + with any person.) + + But this table could also be used as a basis for a Customer or + Employee relationship etc. + """ + __tablename__ = 'person' + + uuid = uuid_column() + + full_name = sa.Column(sa.String(length=100), nullable=False, doc=""" + Full name for the person. Note that this is *required*. + """) + + first_name = sa.Column(sa.String(length=50), nullable=True, doc=""" + The person's first name. + """) + + middle_name = sa.Column(sa.String(length=50), nullable=True, doc=""" + The person's middle name or initial. + """) + + last_name = sa.Column(sa.String(length=50), nullable=True, doc=""" + The person's last name. + """) + + users = orm.relationship( + 'User', + back_populates='person', + # TODO + # cascade_backrefs=False, + doc=""" + List of :class:`~wuttjamaican.db.model.auth.User` accounts for + the person. Typically there is only one user account per + person, but technically multiple are supported. + """) + + def __str__(self): + return self.full_name or "" + + @property + def user(self): + """ + Reference to the "first" + :class:`~wuttjamaican.db.model.auth.User` account for the + person, or ``None``. + + .. warning:: + + Note that the database schema supports multiple users per + person, but this property logic ignores that and will only + ever return "one or none". That might be fine in 99% of + cases, but if multiple accounts exist for a person, the one + returned is indeterminate. + + See :attr:`users` to access the full list. + """ + + # TODO: i'm not crazy about the ambiguity here re: number of + # user accounts a person may have. in particular it's not + # clear *which* user account would be returned, as there is no + # sequence ordinal defined etc. a better approach might be to + # force callers to assume the possibility of multiple + # user accounts per person? (if so, remove this property) + + if self.users: + return self.users[0] diff --git a/tests/db/model/test_auth.py b/tests/db/model/test_auth.py index 29ba802..6bcacc5 100644 --- a/tests/db/model/test_auth.py +++ b/tests/db/model/test_auth.py @@ -5,6 +5,7 @@ from unittest import TestCase try: import sqlalchemy as sa from wuttjamaican.db.model import auth as model + from wuttjamaican.db.model.base import Person except ImportError: pass else: @@ -29,8 +30,16 @@ else: class TestUser(TestCase): - def test_basic(self): + def test_str(self): user = model.User() self.assertEqual(str(user), "") user.username = 'barney' self.assertEqual(str(user), "barney") + + def test_str_with_person(self): + user = model.User() + self.assertEqual(str(user), "") + + person = Person(full_name="Barney Rubble") + user.person = person + self.assertEqual(str(user), "Barney Rubble") diff --git a/tests/db/model/test_base.py b/tests/db/model/test_base.py index 536d2ef..c77dfa2 100644 --- a/tests/db/model/test_base.py +++ b/tests/db/model/test_base.py @@ -5,6 +5,7 @@ from unittest import TestCase try: import sqlalchemy as sa from wuttjamaican.db.model import base as model + from wuttjamaican.db.model.auth import User except ImportError: pass else: @@ -32,3 +33,19 @@ else: self.assertEqual(str(setting), "") setting.name = 'foo' self.assertEqual(str(setting), "foo") + + class TestPerson(TestCase): + + def test_basic(self): + person = model.Person() + self.assertEqual(str(person), "") + person.full_name = "Barney Rubble" + self.assertEqual(str(person), "Barney Rubble") + + def test_users(self): + person = model.Person() + self.assertIsNone(person.user) + + user = User() + person.users.append(user) + self.assertIs(person.user, user)