Updates study model to better match Protocol Builder. Deletes all migrations and starts over, since Alembic has problems migrating changes to existing column constraints.

This commit is contained in:
Aaron Louie 2020-02-28 11:14:30 -05:00
parent 0cc59d0974
commit 4534b0c2df
10 changed files with 117 additions and 178 deletions

6
Pipfile.lock generated
View File

@ -91,10 +91,10 @@
},
"billiard": {
"hashes": [
"sha256:26fd494dc3251f8ce1f5559744f18aeed427fdaf29a75d7baae26752a5d3816f",
"sha256:f4e09366653aa3cb3ae8ed16423f9ba1665ff426f087bcdbbed86bf3664fe02c"
"sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede",
"sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"
],
"version": "==3.6.2.0"
"version": "==3.6.3.0"
},
"blinker": {
"hashes": [

View File

@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional, Union, Tuple, Dict
from connexion import NoContent
from flask import g
@ -8,19 +8,14 @@ from crc.api.common import ApiError, ApiErrorSchema
from crc.api.workflow import __get_workflow_api_model
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
from crc.models.study import StudyModelSchema, StudyModel
from crc.models.workflow import WorkflowModel, WorkflowApiSchema, WorkflowSpecModel
from crc.models.workflow import WorkflowModel, WorkflowApiSchema, WorkflowSpecModel, WorkflowApi
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.protocol_builder import ProtocolBuilderService
@auth.login_required
def all_studies():
user = g.user
""":type: crc.models.user.UserModel"""
update_from_protocol_builder()
db_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all()
return StudyModelSchema(many=True).dump(db_studies)
return update_from_protocol_builder()
@auth.login_required
@ -47,10 +42,11 @@ def update_study(study_id, body):
error = ApiError('unknown_study', 'The study "' + study_id + '" is not recognized.')
return ApiErrorSchema.dump(error), 404
study = StudyModelSchema().load(body, session=session, instance=study)
schema = StudyModelSchema()
study = schema.load(body, session=session, instance=study, partial=True)
session.add(study)
session.commit()
return StudyModelSchema().dump(study)
return schema.dump(study)
@auth.login_required
@ -71,39 +67,49 @@ def update_from_protocol_builder():
""":type: crc.models.user.UserModel"""
# Get studies matching this user from Protocol Builder
pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(user.uid)
pb_studies: List[ProtocolBuilderStudy] = get_user_pb_studies()
# Get studies from the database
db_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all()
db_study_ids = list(map(lambda s: s.id, db_studies))
pb_study_ids = list(map(lambda s: s.id, pb_studies))
pb_study_ids = list(map(lambda s: s['STUDYID'], pb_studies))
# Add studies from Protocol Builder that aren't in the database yet
for pb_study in pb_studies:
if pb_study['HSRNUMBER'] not in db_study_ids:
status = ProtocolBuilderStatus.complete._value_ if pb_study[
'Q_COMPLETE'] else ProtocolBuilderStatus.in_process._value_
add_study({
'id': pb_study['HSRNUMBER'],
'title': pb_study['TITLE'],
'protocol_builder_status': status,
'user_uid': pb_study['NETBADGEID'],
'last_updated': pb_study['DATE_MODIFIED']
})
# Update studies with latest data from Protocol Builder
if pb_study['STUDYID'] in db_study_ids:
update_study(pb_study['STUDYID'], map_pb_study_to_study(pb_study))
# Add studies from Protocol Builder that aren't in the database yet
else:
new_study = map_pb_study_to_study(pb_study)
add_study(new_study)
# Mark studies as inactive that are no longer in Protocol Builder
for study_id in db_study_ids:
if study_id not in pb_study_ids:
update_study(study_id=study_id, body={'inactive': True})
# Return updated studies
updated_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all()
results = StudyModelSchema(many=True).dump(updated_studies)
return results
@auth.login_required
def post_update_study_from_protocol_builder(study_id):
"""Update a single study based on data received from
the protocol builder."""
# todo: Actually get data from an external service here
pb_studies: List[ProtocolBuilderStudy] = get_user_pb_studies()
for pb_study in pb_studies:
if pb_study['STUDYID'] == study_id:
return update_study(study_id, map_pb_study_to_study(pb_study))
return NoContent, 304
@auth.login_required
def get_study_workflows(study_id):
workflow_models = session.query(WorkflowModel).filter_by(study_id=study_id).all()
api_models = []
@ -115,6 +121,7 @@ def get_study_workflows(study_id):
return schema.dump(api_models)
@auth.login_required
def add_workflow_to_study(study_id, body):
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id=body["id"]).first()
if workflow_spec_model is None:
@ -122,3 +129,38 @@ def add_workflow_to_study(study_id, body):
return ApiErrorSchema.dump(error), 404
processor = WorkflowProcessor.create(study_id, workflow_spec_model.id)
return WorkflowApiSchema().dump(__get_workflow_api_model(processor))
@auth.login_required
def get_user_pb_studies() -> List[ProtocolBuilderStudy]:
"""Get studies from Protocol Builder matching the given user"""
user = g.user
""":type: crc.models.user.UserModel"""
return ProtocolBuilderService.get_studies(user.uid)
def map_pb_study_to_study(pb_study):
"""Translates the given dict of ProtocolBuilderStudy properties to dict of StudyModel attributes"""
prop_map = {
'STUDYID': 'id',
'HSRNUMBER': 'hsr_number',
'TITLE': 'title',
'NETBADGEID': 'user_uid',
'DATE_MODIFIED': 'last_updated',
}
study_info = {}
# Translate Protocol Builder property names to Study attributes
for k, v in pb_study.items():
if k in prop_map:
study_info[prop_map[k]] = v
if pb_study['Q_COMPLETE']:
study_info['protocol_builder_status'] = ProtocolBuilderStatus.complete._value_
else:
study_info['protocol_builder_status'] = ProtocolBuilderStatus.in_process._value_
return study_info

View File

@ -12,12 +12,14 @@ class StudyModel(db.Model):
title = db.Column(db.String)
last_updated = db.Column(db.DateTime(timezone=True), default=func.now())
protocol_builder_status = db.Column(db.Enum(ProtocolBuilderStatus))
primary_investigator_id = db.Column(db.String)
sponsor = db.Column(db.String)
ind_number = db.Column(db.String)
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=True)
investigator_uids = db.Column(db.ARRAY(db.String))
primary_investigator_id = db.Column(db.String, nullable=True)
sponsor = db.Column(db.String, nullable=True)
hsr_number = db.Column(db.String, nullable=True)
ind_number = db.Column(db.String, nullable=True)
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=False)
investigator_uids = db.Column(db.ARRAY(db.String), nullable=True)
inactive = db.Column(db.Boolean, default=False)
requirements = db.Column(db.ARRAY(db.Integer), nullable=True)
class StudyModelSchema(ModelSchema):

View File

@ -1,36 +0,0 @@
"""empty message
Revision ID: 0a6e0b829398
Revises: ad5483cb7f3b
Create Date: 2020-02-20 15:42:16.473470
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0a6e0b829398'
down_revision = 'ad5483cb7f3b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('affiliation', sa.String(), nullable=True))
op.add_column('user', sa.Column('eppn', sa.String(), nullable=True))
op.add_column('user', sa.Column('first_name', sa.String(), nullable=True))
op.add_column('user', sa.Column('last_name', sa.String(), nullable=True))
op.add_column('user', sa.Column('title', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'title')
op.drop_column('user', 'last_name')
op.drop_column('user', 'first_name')
op.drop_column('user', 'eppn')
op.drop_column('user', 'affiliation')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 2c88e49d0ffc
Revises: 726d09a4fa0c
Create Date: 2020-02-27 11:17:01.768161
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2c88e49d0ffc'
down_revision = '726d09a4fa0c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('study', sa.Column('inactive', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('study', 'inactive')
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""empty message
Revision ID: 726d09a4fa0c
Revises: 0a6e0b829398
Create Date: 2020-02-26 16:35:05.854328
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '726d09a4fa0c'
down_revision = '0a6e0b829398'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('study', sa.Column('investigator_uids', sa.ARRAY(sa.String()), nullable=True))
op.add_column('study', sa.Column('user_uid', sa.String(), nullable=True))
op.create_foreign_key(None, 'study', 'user', ['user_uid'], ['uid'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'study', type_='foreignkey')
op.drop_column('study', 'user_uid')
op.drop_column('study', 'investigator_uids')
# ### end Alembic commands ###

View File

@ -1,35 +0,0 @@
"""empty message
Revision ID: ad5483cb7f3b
Revises: 02fcf09d9085
Create Date: 2020-02-19 11:59:09.948767
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ad5483cb7f3b'
down_revision = '02fcf09d9085'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uid', sa.String(), nullable=True),
sa.Column('email_address', sa.String(), nullable=True),
sa.Column('display_name', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uid')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: 02fcf09d9085
Revises:
Create Date: 2020-02-05 17:18:35.324675
Revision ID: cb3a03c10a0e
Revises:
Create Date: 2020-02-28 11:12:56.150837
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '02fcf09d9085'
revision = 'cb3a03c10a0e'
down_revision = None
branch_labels = None
depends_on = None
@ -18,15 +18,18 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('study',
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uid', sa.String(), nullable=True),
sa.Column('email_address', sa.String(), nullable=True),
sa.Column('display_name', sa.String(), nullable=True),
sa.Column('affiliation', sa.String(), nullable=True),
sa.Column('eppn', sa.String(), nullable=True),
sa.Column('first_name', sa.String(), nullable=True),
sa.Column('last_name', sa.String(), nullable=True),
sa.Column('title', sa.String(), nullable=True),
sa.Column('last_updated', sa.DateTime(timezone=True), nullable=True),
sa.Column('protocol_builder_status', sa.Enum('out_of_date', 'in_process', 'complete', 'updating', name='protocolbuilderstatus'), nullable=True),
sa.Column('primary_investigator_id', sa.String(), nullable=True),
sa.Column('sponsor', sa.String(), nullable=True),
sa.Column('ind_number', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uid')
)
op.create_table('workflow_spec',
sa.Column('id', sa.String(), nullable=False),
@ -36,6 +39,22 @@ def upgrade():
sa.Column('primary_process_id', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('study',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=True),
sa.Column('last_updated', sa.DateTime(timezone=True), nullable=True),
sa.Column('protocol_builder_status', sa.Enum('out_of_date', 'in_process', 'complete', 'updating', name='protocolbuilderstatus'), nullable=True),
sa.Column('primary_investigator_id', sa.String(), nullable=True),
sa.Column('sponsor', sa.String(), nullable=True),
sa.Column('hsr_number', sa.String(), nullable=True),
sa.Column('ind_number', sa.String(), nullable=True),
sa.Column('user_uid', sa.String(), nullable=False),
sa.Column('investigator_uids', sa.ARRAY(sa.String()), nullable=True),
sa.Column('inactive', sa.Boolean(), nullable=True),
sa.Column('requirements', sa.ARRAY(sa.Integer()), nullable=True),
sa.ForeignKeyConstraint(['user_uid'], ['user.uid'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('workflow',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('bpmn_workflow_json', sa.JSON(), nullable=True),
@ -79,6 +98,7 @@ def downgrade():
op.drop_table('file_data')
op.drop_table('file')
op.drop_table('workflow')
op.drop_table('workflow_spec')
op.drop_table('study')
op.drop_table('workflow_spec')
op.drop_table('user')
# ### end Alembic commands ###

View File

@ -27,6 +27,7 @@ class TestStudyApi(BaseTest):
"primary_investigator_id": "tricia.marie.mcmillan@heartofgold.edu",
"sponsor": "Sirius Cybernetics Corporation",
"ind_number": "567890",
"user_uid": "dhf8r",
}
rv = self.app.post('/v1.0/study',
content_type="application/json",
@ -41,6 +42,7 @@ class TestStudyApi(BaseTest):
self.assertEqual(study["primary_investigator_id"], db_study.primary_investigator_id)
self.assertEqual(study["sponsor"], db_study.sponsor)
self.assertEqual(study["ind_number"], db_study.ind_number)
self.assertEqual(study["user_uid"], db_study.user_uid)
def test_update_study(self):
self.load_example_data()

View File

@ -13,8 +13,12 @@ class TestTasksApi(BaseTest):
def create_workflow(self, workflow_name):
study = session.query(StudyModel).first()
spec = session.query(WorkflowSpecModel).filter_by(id=workflow_name).first()
self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json",
data=json.dumps(WorkflowSpecModelSchema().dump(spec)))
rv = self.app.post(
'/v1.0/study/%i/workflows' % study.id,
headers=self.logged_in_headers(),
content_type="application/json",
data=json.dumps(WorkflowSpecModelSchema().dump(spec)))
self.assert_success(rv)
workflow = session.query(WorkflowModel).filter_by(study_id=study.id, workflow_spec_id=workflow_name).first()
return workflow
@ -183,4 +187,4 @@ class TestTasksApi(BaseTest):
workflow_api = self.get_workflow_api(workflow)
self.assertEqual("EndEvent_0u1cgrf", workflow_api.next_task['name'])
self.assertIsNotNone(workflow_api.next_task['documentation'])
self.assertTrue("norris" in workflow_api.next_task['documentation'])
self.assertTrue("norris" in workflow_api.next_task['documentation'])