diff --git a/backend/database/migrations/versions/335e07a98bc8_create_user_tables.py b/backend/database/migrations/versions/335e07a98bc8_create_user_tables.py index 54a3148..94946ea 100644 --- a/backend/database/migrations/versions/335e07a98bc8_create_user_tables.py +++ b/backend/database/migrations/versions/335e07a98bc8_create_user_tables.py @@ -6,7 +6,7 @@ Create Date: 2023-05-12 19:59:22.188464 """ from alembic import op -from sqlalchemy import Column, ForeignKey, Integer, String, DateTime, Date, Enum, CheckConstraint +from sqlalchemy import Column, ForeignKey, Integer, String, DateTime, Date, Enum from sqlalchemy.sql.functions import now from backend.models.users import Gender diff --git a/backend/database/migrations/versions/7e5a8cabd3a4_create_device_tables.py b/backend/database/migrations/versions/7e5a8cabd3a4_create_device_tables.py new file mode 100644 index 0000000..8978bed --- /dev/null +++ b/backend/database/migrations/versions/7e5a8cabd3a4_create_device_tables.py @@ -0,0 +1,61 @@ +"""create device tables + +Revision ID: 7e5a8cabd3a4 +Revises: 335e07a98bc8 +Create Date: 2023-05-12 21:59:26.867894 + +""" +from alembic import op +from sqlalchemy import Column, ForeignKey, Integer, String, DateTime +from sqlalchemy.sql.functions import now + + +# revision identifiers, used by Alembic. +revision = '7e5a8cabd3a4' +down_revision = '335e07a98bc8' +branch_labels = None +depends_on = '335e07a98bc8' + + +def upgrade() -> None: + + device_models_table = op.create_table( + 'device_models', + Column('id', Integer, primary_key=True, autoincrement=True, index=True), + Column('name', String, unique=True, nullable=False), + Column('picture_code', String, unique=True, nullable=False), + ) + + op.create_table( + 'devices', + Column('id', Integer, primary_key=True, autoincrement=True, index=True), + Column('added', DateTime(timezone=True), nullable=False, server_default=now()), + Column('last_seen', DateTime(timezone=True), nullable=True, server_default=None), + Column('model_id', Integer, ForeignKey('device_models.id', ondelete="CASCADE"), nullable=False), + Column('owner_id', Integer, ForeignKey('patients.user_id', ondelete="CASCADE"), nullable=False), + ) + + # Fill table with known device models. + # Picture codes are generated using `python -c "import uuid; print(uuid.uuid4())"` + op.bulk_insert( + device_models_table, + [ + { + "name": "ScanWatch", + "picture_code": "133184d2-3b7c-4589-bc76-9736455a56b9", + }, + { + "name": "Thermo", + "picture_code": "3b0cd959-a293-488f-94d6-5f6dc0f775f3", + }, + { + "name": "BPM Core", + "picture_code": "36b60fc2-567f-4e7e-b76e-2a9006c0f0fb", + }, + ] + ) + + +def downgrade() -> None: + op.drop_table('devices') + op.drop_table('device_models') diff --git a/backend/models/devices.py b/backend/models/devices.py new file mode 100644 index 0000000..c48751c --- /dev/null +++ b/backend/models/devices.py @@ -0,0 +1,36 @@ +"""This module defines the SQL data model for devices.""" + +import enum + +from sqlalchemy import Column, ForeignKey, Integer, String, DateTime, Uuid +from sqlalchemy.sql.functions import now +from sqlalchemy.orm import relationship + +from backend.database.engine import Base + + +class DeviceModel(Base): + """Model for the device_model table.""" + + __tablename__ = "device_models" + + id = Column('id', Integer, primary_key=True, autoincrement=True, index=True) + name = Column('name', String, unique=True, nullable=False) + picture_code = Column('picture_code', String, unique=True, nullable=False) + + instances = relationship("Device", back_populates="model", uselist=True, cascade="all, delete") + + +class Device(Base): + """Model for the devices table.""" + + __tablename__ = "devices" + + id = Column('id', Integer, primary_key=True, autoincrement=True, index=True) + added = Column('added', DateTime(timezone=True), nullable=False, server_default=now()) + last_seen = Column('last_seen', DateTime(timezone=True), nullable=True, server_default=None) + model_id = Column('model_id', Integer, ForeignKey('device_models.id', ondelete="CASCADE"), nullable=False) + owner_id = Column('owner_id', Integer, ForeignKey('patients.user_id', ondelete="CASCADE"), nullable=False) + + model = relationship(DeviceModel, back_populates="instances", uselist=False) + owner = relationship("Patient", back_populates="devices", uselist=False) diff --git a/backend/models/users.py b/backend/models/users.py index de3cfd4..a3f3f19 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -1,4 +1,4 @@ -"""This module defines the SQL user model for users. +"""This module defines the SQL data model for users. All users are either Patients or Administrators. """ @@ -10,6 +10,7 @@ from sqlalchemy.sql.functions import now from sqlalchemy.orm import relationship from backend.database.engine import Base +from backend.models.devices import Device class User(Base): @@ -17,13 +18,13 @@ class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, autoincrement=True, index=True) - email = Column(String, unique=True, nullable=False) - password = Column(String, nullable=False) - created = Column(DateTime(timezone=True), nullable=False, server_default=now()) - updated = Column(DateTime(timezone=True), nullable=False, server_default=now(), onupdate=now()) - first_name = Column(String, nullable=False) - last_name = Column(String, nullable=False) + id = Column('id', Integer, primary_key=True, autoincrement=True, index=True) + email = Column('email', String, unique=True, nullable=False) + password = Column('password', String, nullable=False) + created = Column('created', DateTime(timezone=True), nullable=False, server_default=now()) + updated = Column('updated', DateTime(timezone=True), nullable=False, server_default=now(), onupdate=now()) + first_name = Column('first_name', String, nullable=False) + last_name = Column('last_name', String, nullable=False) administrator = relationship("Administrator", back_populates="user", uselist=False, cascade="all, delete") patient = relationship("Patient", back_populates="user", uselist=False, cascade="all, delete") @@ -34,8 +35,8 @@ class Administrator(Base): __tablename__ = "administrators" - user_id = Column(Integer, ForeignKey('users.id', ondelete="CASCADE"), primary_key=True,) - user = relationship("User", back_populates="administrator", uselist=False, cascade="all, delete") + user_id = Column('user_id', Integer, ForeignKey('users.id', ondelete="CASCADE"), primary_key=True) + user = relationship(User, back_populates="administrator", uselist=False, cascade="all, delete") class Gender(enum.Enum): @@ -50,7 +51,9 @@ class Patient(Base): __tablename__ = "patients" - user_id = Column(Integer, ForeignKey('users.id', ondelete="CASCADE"), primary_key=True) - user = relationship("User", back_populates="patient", uselist=False, cascade="all, delete") - date_of_birth = Column(Date, nullable=False) - gender = Column(Enum(Gender), nullable=False) + user_id = Column('user_id', Integer, ForeignKey('users.id', ondelete="CASCADE"), primary_key=True) + date_of_birth = Column('date_of_birth', Date, nullable=False) + gender = Column('gender', Enum(Gender), nullable=False) + + user = relationship(User, back_populates="patient", uselist=False, cascade="all, delete") + devices = relationship(Device, back_populates="owner", uselist=True, cascade="all, delete") diff --git a/backend/schemas/devices.py b/backend/schemas/devices.py new file mode 100644 index 0000000..78e474c --- /dev/null +++ b/backend/schemas/devices.py @@ -0,0 +1,48 @@ +"""This module declareds the pydantic schema representation for devices.""" + +from datetime import datetime + +from pydantic import BaseModel, validator + + +class DeviceModel(BaseModel): + id: int + name: str + picture_code: str + + @validator('name') + def assert_name_is_valid(cls, name): + if not len(name): + raise ValueError("Name must not be empty.") + return name + + @validator('picture_code') + def assert_picture_is_valid(cls, picture): + if not len(picture): + raise ValueError("Picture must not be empty.") + return picture + + class Config: + orm_mode = True + + +class Device(BaseModel): + id: int + added: datetime + last_seen: datetime + model: DeviceModel + + @validator('added') + def assert_added_is_valid(cls, added): + if added >= datetime.now(added.tzinfo): + raise ValueError("Date added cannot be in the future.") + return added + + @validator('last_seen') + def assert_last_seen_is_valid(cls, last_seen): + if last_seen >= datetime.now(last_seen.tzinfo): + raise ValueError("Date last seen cannot be in the future.") + return last_seen + + class Config: + orm_mode = True diff --git a/backend/schemas/users.py b/backend/schemas/users.py index f874483..d557ec4 100644 --- a/backend/schemas/users.py +++ b/backend/schemas/users.py @@ -1,4 +1,4 @@ -"""This module declared the pydantic schema representation for users. +"""This module declareds the pydantic schema representation for users. Note that it is not a direct representation of how users are modeled in the database. Instead, the User schema class contains all attributes from all user classes @@ -15,6 +15,7 @@ from typing import Optional from pydantic import BaseModel, validator from backend.models.users import Gender +from backend.schemas.devices import Device class AbstractUserInfoValidation(BaseModel, ABC): @@ -141,6 +142,7 @@ class User(AbstractUser): id: int created: datetime updated: datetime + devices: list[Device] = [] class Config: orm_mode = True diff --git a/public/static/images/devices/133184d2-3b7c-4589-bc76-9736455a56b9.webp b/public/static/images/devices/133184d2-3b7c-4589-bc76-9736455a56b9.webp new file mode 100644 index 0000000..4ac4279 Binary files /dev/null and b/public/static/images/devices/133184d2-3b7c-4589-bc76-9736455a56b9.webp differ diff --git a/public/static/images/devices/36b60fc2-567f-4e7e-b76e-2a9006c0f0fb.webp b/public/static/images/devices/36b60fc2-567f-4e7e-b76e-2a9006c0f0fb.webp new file mode 100644 index 0000000..23deb1d Binary files /dev/null and b/public/static/images/devices/36b60fc2-567f-4e7e-b76e-2a9006c0f0fb.webp differ diff --git a/public/static/images/devices/3b0cd959-a293-488f-94d6-5f6dc0f775f3.webp b/public/static/images/devices/3b0cd959-a293-488f-94d6-5f6dc0f775f3.webp new file mode 100644 index 0000000..91f4713 Binary files /dev/null and b/public/static/images/devices/3b0cd959-a293-488f-94d6-5f6dc0f775f3.webp differ diff --git a/public/static/images/tux.svg b/public/static/images/tux.svg deleted file mode 100644 index 54b7a7d..0000000 --- a/public/static/images/tux.svg +++ /dev/null @@ -1,363 +0,0 @@ - - - Tux - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file