-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
654 additions
and
134 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
portal/migrations/versions/2e9b9e696bb8_adherencedata_table.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
"""AdherenceData table | ||
Revision ID: 2e9b9e696bb8 | ||
Revises: ccb67176c56f | ||
Create Date: 2023-06-15 17:45:43.699277 | ||
""" | ||
from alembic import op | ||
import sqlalchemy as sa | ||
from sqlalchemy.dialects import postgresql | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = '2e9b9e696bb8' | ||
down_revision = 'ccb67176c56f' | ||
|
||
|
||
def upgrade(): | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.create_table('adherence_data', | ||
sa.Column('id', sa.Integer(), nullable=False), | ||
sa.Column('patient_id', sa.Integer(), nullable=False), | ||
sa.Column('rs_id_visit', sa.Text(), nullable=False), | ||
sa.Column('valid_till', sa.DateTime(), nullable=False), | ||
sa.Column('data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), | ||
sa.ForeignKeyConstraint(['patient_id'], ['users.id'], ), | ||
sa.PrimaryKeyConstraint('id'), | ||
sa.UniqueConstraint('patient_id', 'rs_id_visit', name='_adherence_unique_patient_visit') | ||
) | ||
op.create_index(op.f('ix_adherence_data_patient_id'), 'adherence_data', ['patient_id'], unique=False) | ||
op.create_index(op.f('ix_adherence_data_rs_id_visit'), 'adherence_data', ['rs_id_visit'], unique=False) | ||
op.create_index(op.f('ix_adherence_data_valid_till'), 'adherence_data', ['valid_till'], unique=False) | ||
# ### end Alembic commands ### | ||
|
||
|
||
def downgrade(): | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.drop_index(op.f('ix_adherence_data_valid_till'), table_name='adherence_data') | ||
op.drop_index(op.f('ix_adherence_data_rs_id_visit'), table_name='adherence_data') | ||
op.drop_index(op.f('ix_adherence_data_patient_id'), table_name='adherence_data') | ||
op.drop_table('adherence_data') | ||
# ### end Alembic commands ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
""" model data for adherence reports """ | ||
from datetime import datetime, timedelta | ||
from sqlalchemy.dialects.postgresql import JSONB | ||
from sqlalchemy import UniqueConstraint | ||
|
||
from ..database import db | ||
|
||
|
||
class AdherenceData(db.Model): | ||
""" Cached adherence report data | ||
Full history adherence data is expensive to generate, retain between reports. | ||
Cache reportable data in simple JSON structure, maintaining keys for lookup | ||
and invalidation timestamps. | ||
rs_id_visit: the numeric rs_id and visit month string joined with a colon | ||
valid_till: old history data never changes, unless an external event such | ||
as a user's consent date or organization research protocol undergoes | ||
change. active visits require more frequent updates but are considered | ||
fresh enough for days. client code sets valid_till as appropriate. | ||
""" | ||
__tablename__ = 'adherence_data' | ||
id = db.Column(db.Integer, primary_key=True) | ||
patient_id = db.Column(db.ForeignKey('users.id'), index=True, nullable=False) | ||
rs_id_visit = db.Column( | ||
db.Text, index=True, nullable=False, | ||
doc="rs_id:visit_name") | ||
valid_till = db.Column( | ||
db.DateTime, nullable=False, index=True, | ||
doc="cached values good till time passed") | ||
data = db.Column(JSONB) | ||
|
||
__table_args__ = (UniqueConstraint( | ||
'patient_id', 'rs_id_visit', name='_adherence_unique_patient_visit'),) | ||
|
||
@staticmethod | ||
def rs_visit_string(rs_id, visit_string): | ||
"""trivial helper to build rs_id_visit string into desired format""" | ||
assert isinstance(rs_id, int) | ||
assert visit_string | ||
return f"{rs_id}:{visit_string}" | ||
|
||
def rs_visit_parse(self): | ||
"""break parts of rs_id and visit_string out of rs_id_visit field""" | ||
rs_id, visit_string = self.rs_id_visit.split(':') | ||
assert visit_string | ||
return int(rs_id), visit_string | ||
|
||
@staticmethod | ||
def fetch(patient_id, rs_id_visit): | ||
"""shortcut for common lookup need | ||
:return: populated AdherenceData instance if found, None otherwise | ||
""" | ||
result = AdherenceData.query.filter( | ||
AdherenceData.patient_id == patient_id).filter( | ||
AdherenceData.rs_id_visit == rs_id_visit).first() | ||
if result: | ||
assert result.valid_till > datetime.utcnow() | ||
return result | ||
|
||
@staticmethod | ||
def persist(patient_id, rs_id_visit, valid_for_days, data): | ||
"""shortcut to persist a row, returns new instance""" | ||
import json | ||
valid_till = datetime.utcnow() + timedelta(days=valid_for_days) | ||
for k, v in data.items(): | ||
try: | ||
json.dumps(k) | ||
json.dumps(v) | ||
except TypeError: | ||
raise ValueError(f"couldn't encode {k}:{v}, {type(v)}") | ||
|
||
record = AdherenceData( | ||
patient_id=patient_id, | ||
rs_id_visit=rs_id_visit, | ||
valid_till=valid_till, | ||
data=data) | ||
db.session.add(record) | ||
db.session.commit() | ||
return db.session.merge(record) | ||
|
||
|
||
def sort_by_visit_key(d): | ||
"""Given dict returns ordered list of values sorted by key | ||
Sort by key using the following rules: | ||
"Baseline" comes first | ||
"Indefinite" comes last | ||
"Month <n>" in the middle, sorted by integer value | ||
:returns: list of values sorted by keys | ||
""" | ||
def sort_key(key): | ||
if key == 'Baseline': | ||
return 0, 0 | ||
elif key == 'Indefinite': | ||
return 2, 0 | ||
else: | ||
month, num = key.split(" ") | ||
assert month == "Month" | ||
return 1, int(num) | ||
|
||
sorted_keys = sorted(d.keys(), key=sort_key) | ||
sorted_values = [d[key] for key in sorted_keys] | ||
return sorted_values | ||
|
||
|
||
def sorted_adherence_data(patient_id, research_study_id): | ||
"""Shortcut to obtain ordered list for given patient:research_study""" | ||
rows = AdherenceData.query.filter( | ||
AdherenceData.patient_id == patient_id).filter( | ||
AdherenceData.rs_id_visit.like(f"{research_study_id}%")) | ||
# Sort locally, given complicated sort function | ||
presorted = {row.rs_id_visit.split(':')[1]: row.data for row in rows} | ||
return sort_by_visit_key(presorted) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.