ba-thesis/backend/schemas/users.py

147 lines
4.8 KiB
Python

"""This module declared 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
as optional attributes.
I haven't figured out a smart way to do this with pydantic yet, so behold the
inheritance hellhole below.
"""
from datetime import datetime, date
from abc import ABC
from typing import Optional
from pydantic import BaseModel, validator
from backend.models.users import Gender
class AbstractUserInfoValidation(BaseModel, ABC):
"""Base class providing common field validators."""
@validator('email', check_fields=False)
def assert_email_is_valid(cls, email):
if email is not None:
if not len(email):
raise ValueError("Email must not be empty.")
# TODO implement more robust check
return email
@validator('first_name', check_fields=False)
def assert_first_name_is_valid(cls, first_name):
if first_name is not None:
if not len(first_name):
raise ValueError("First Name must not be empty.")
return first_name
@validator('last_name', check_fields=False)
def assert_last_name_is_valid(cls, last_name):
if last_name is not None:
if not len(last_name):
raise ValueError("Last Name must not be empty.")
return last_name
@validator('date_of_birth', check_fields=False)
def assert_dob_is_valid(cls, dob):
if dob is not None:
if dob >= date.today():
raise ValueError("Date of birth cannot be in the future.")
return dob
class AbstractUser(AbstractUserInfoValidation, ABC):
"""Base class for attributes common to user creation and user representation.
A user must be either a patient or an administrator. If a user is a patient,
they must specify valid 'date_of_birth' and 'gender' attributes.
"""
email: str
first_name: str
last_name: str
gender: Optional[Gender]
date_of_birth: Optional[date]
is_patient: Optional[bool]
is_admin: Optional[bool]
@validator('is_admin')
def assert_tegridy(cls, is_admin, values):
"""Ensures logical model integrity when optional fields are set."""
if values['is_patient']:
if is_admin:
raise ValueError('User cannot be both patient and admin.')
for key in ['gender', 'date_of_birth']:
if key not in values or values[key] is None:
raise ValueError(f"Must specify key '{key}' for patients.")
if not values['is_patient'] and not is_admin:
raise ValueError(f'User must either be patient or admin.')
return is_admin
class UserCreate(AbstractUser):
"""Scheme for user creation."""
password: str
password_confirmation: str
@validator('password_confirmation')
def assert_passwords_match(cls, password_confirmation, values):
if not password_confirmation == values['password']:
raise ValueError("Passwords do not match.")
if len(password_confirmation) < 1:
# TODO use more robust password rules
raise ValueError("Password must not be empty.")
return password_confirmation
class UserUpdate(AbstractUserInfoValidation):
"""Scheme for user updates.
All fields here are optional, but passwords must match if at least one was
provided.
Note that even administrator updates can specify 'gender' and 'date_of_birth'
fields, the function inserting the update into the db should handle this (and
just ignore the fields).
Switching user types is prohibited.
"""
email: Optional[str]
first_name: Optional[str]
last_name: Optional[str]
gender: Optional[Gender]
date_of_birth: Optional[date]
password: Optional[str] = None
password_confirmation: Optional[str] = None
@validator('password_confirmation')
def assert_passwords_match_or_are_both_none(cls, password_confirmation, values):
password = values.get('password')
if None not in [password, password_confirmation]:
if not password == password_confirmation:
raise ValueError("Passwords do not match.")
if len(password_confirmation) < 1:
# TODO use more robust password rules
raise ValueError("Password must not be empty.")
return password_confirmation
class User(AbstractUser):
"""Final representation of all types of users, wrapped into one User schema.
The id, created and updated fields are filled by the db during creation, so
they are not needed in the parent classes.
"""
id: int
created: datetime
updated: datetime
class Config:
orm_mode = True