"""
Classes to model users (doctors) and patients.
"""
from collections import defaultdict
from datetime import datetime
import numpy as np
from flask_login import UserMixin
from werkzeug.security import check_password_hash
from werkzeug.security import generate_password_hash
from app import db
from app import login_manager
[docs]class BasicInfo:
"""Base class for modeling users and patients."""
id = db.Column(db.Integer, primary_key=True)
firstname = db.Column(db.String(50), unique=False, nullable=False)
lastname = db.Column(db.String(50), unique=False, nullable=False)
email = db.Column(db.String(80), unique=True, nullable=False)
[docs]class User(db.Model, UserMixin, BasicInfo):
"""User model with functions to set and check password when logging in."""
username = db.Column(db.String(20), unique=True, nullable=False)
password_hash = db.Column(db.String(120), nullable=False)
# One-to-many relationship
patients = db.relationship("Patient", backref="user", lazy=True)
def __init__(self, firstname, lastname, username, email, password):
self.firstname = firstname
self.lastname = lastname
self.username = username
self.email = email
self.set_password(password)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
[docs]class Patient(db.Model, BasicInfo):
"""Defines a Patient, which has a foreign key pointed to User (doctor)."""
age = db.Column(db.Integer, unique=False, nullable=False)
weight = db.Column(db.Integer, unique=False, nullable=False)
# Foreign key
doctor_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
# One-to-many relationship
prescriptions = db.relationship("Prescription", backref="patient", lazy=True)
def __init__(self, firstname, lastname, email, age, weight, user=None):
self.firstname = firstname
self.lastname = lastname
self.email = email
self.age = age
self.weight = weight
self.user = user
[docs] def all_intakes(self, start=None, end=None):
"""
Return Intake objects associated with this Patient.
start: datetime - optional start date for filtering
end: datetime - optional end date for filtering
TODO: Implement start, end filtering
NOTE: There's probably a better way of doing this. Perhaps setting Patient
as a foreign key in Intake?
"""
all_intakes = []
for rx in self.prescriptions:
all_intakes += rx.intakes
return all_intakes
[docs] def adherence_stats(self, date=datetime.now()):
"""
Return adherence statistics on a per-Prescription basis.
return: dict - <prescription ID>: <details>
Example:
{
1: {
'frac_on_time': 0.932,
'frac_required_intakes': 0.976
},
19: {
'frac_on_time': 0.389,
'frac_required_intakes': 1.0
},
...
}
"""
stats = defaultdict(lambda: defaultdict(float))
for rx in self.prescriptions:
stats[rx.id]["frac_on_time"] = rx.frac_on_time(date=date)
stats[rx.id]["frac_required_intakes"] = rx.frac_required_intakes(date=date)
return dict(stats)
[docs] def frac_adhering_prescriptions(
self, on_time_threshold=0.9, required_intakes_threshold=0.9
):
"""
Return fraction of prescriptions for this Patient that are deemed
adherent.
Adherence requires Intakes to be recorded on time and on track
(i.e. medications aren't missed).
on_time_threshold: float - fraction on time Intakes needed for
this prescription to be deemed adherent
required_intakes_threshold: float - fraction Intakes actually
recorded, out of all prescribed Intakes since start date.
"""
if len(self.prescriptions) == 0:
return {}
adherence = {}
frac_on_time_by_rx = []
frac_required_intakes_by_rx = []
stats = self.adherence_stats()
for rx_id, details in stats.items():
frac_on_time_by_rx.append(details["frac_on_time"])
frac_required_intakes_by_rx.append(details["frac_required_intakes"])
adherence["on_time"] = np.sum(
np.array(frac_on_time_by_rx) >= on_time_threshold
) / len(frac_on_time_by_rx)
adherence["required_intakes"] = np.sum(
np.array(frac_required_intakes_by_rx) >= required_intakes_threshold
) / len(frac_required_intakes_by_rx)
return adherence
[docs] def is_adherent(
self, on_time_threshold=0.9, required_intakes_threshold=0.9, date=datetime.now()
):
"""
Whether or not a patient is deemed adherent based on their
prescription adherence.
on_time_threshold: float - fraction on time Intakes needed for
this prescription to be deemed adherent
required_intakes_threshold: float - fraction Intakes actually
recorded, out of all prescribed Intakes since start date.
"""
if np.all([p.start_date > datetime.now() for p in self.prescriptions]):
return True
stats = self.adherence_stats(date=date)
for rx_id, details in stats.items():
if list(filter(lambda rx: rx.id == rx_id, self.prescriptions))[
0
].start_date <= datetime.now() and (
details["frac_on_time"] <= on_time_threshold
or details["frac_required_intakes"] <= required_intakes_threshold
):
return False
return True
# Load user object from user ID stored in the current session
[docs]@login_manager.user_loader
def load_user(id):
"""
This callback is used to reload the user object
from the user ID stored in the session.
"""
return User.query.get(int(id))