147 lines
4.8 KiB
Python
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
|