Merge pull request #179 from sartography/feature/proper_changes_history

Proper changes history
This commit is contained in:
Aaron Louie 2020-08-12 10:14:01 -04:00 committed by GitHub
commit e17e7e6975
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 21 deletions

View File

@ -13,6 +13,7 @@ from crc.models.file import FileModel, SimpleFileSchema, FileSchema
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \ from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \
WorkflowModel WorkflowModel
from crc.services.user_service import UserService
class StudyStatus(enum.Enum): class StudyStatus(enum.Enum):
@ -28,6 +29,11 @@ class IrbStatus(enum.Enum):
hsr_assigned = 'hsr number assigned' hsr_assigned = 'hsr number assigned'
class StudyEventType(enum.Enum):
user = 'user'
automatic = 'automatic'
class StudyModel(db.Model): class StudyModel(db.Model):
__tablename__ = 'study' __tablename__ = 'study'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -44,6 +50,8 @@ class StudyModel(db.Model):
requirements = db.Column(db.ARRAY(db.Integer), nullable=True) requirements = db.Column(db.ARRAY(db.Integer), nullable=True)
on_hold = db.Column(db.Boolean, default=False) on_hold = db.Column(db.Boolean, default=False)
enrollment_date = db.Column(db.DateTime(timezone=True), nullable=True) enrollment_date = db.Column(db.DateTime(timezone=True), nullable=True)
# events = db.relationship("TaskEventModel")
events_history = db.relationship("StudyEvent", cascade="all, delete, delete-orphan")
def update_from_protocol_builder(self, pbs: ProtocolBuilderStudy): def update_from_protocol_builder(self, pbs: ProtocolBuilderStudy):
self.hsr_number = pbs.HSRNUMBER self.hsr_number = pbs.HSRNUMBER
@ -60,6 +68,18 @@ class StudyModel(db.Model):
self.status = StudyStatus.hold self.status = StudyStatus.hold
class StudyEvent(db.Model):
__tablename__ = 'study_event'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False)
study = db.relationship(StudyModel, back_populates='events_history')
create_date = db.Column(db.DateTime(timezone=True), default=func.now())
status = db.Column(db.Enum(StudyStatus))
comment = db.Column(db.String, default='')
event_type = db.Column(db.Enum(StudyEventType))
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=True)
class WorkflowMetadata(object): class WorkflowMetadata(object):
def __init__(self, id, name = None, display_name = None, description = None, spec_version = None, def __init__(self, id, name = None, display_name = None, description = None, spec_version = None,
category_id = None, category_display_name = None, state: WorkflowState = None, category_id = None, category_display_name = None, state: WorkflowState = None,
@ -128,15 +148,16 @@ class CategorySchema(ma.Schema):
class Study(object): class Study(object):
def __init__(self, title, last_updated, primary_investigator_id, user_uid, def __init__(self, title, last_updated, primary_investigator_id, user_uid,
id=None, status=None, irb_status=None, id=None, status=None, irb_status=None, comment="",
sponsor="", hsr_number="", ind_number="", categories=[], sponsor="", hsr_number="", ind_number="", categories=[],
files=[], approvals=[], enrollment_date=None, **argsv): files=[], approvals=[], enrollment_date=None, events_history=[], **argsv):
self.id = id self.id = id
self.user_uid = user_uid self.user_uid = user_uid
self.title = title self.title = title
self.last_updated = last_updated self.last_updated = last_updated
self.status = status self.status = status
self.irb_status = irb_status self.irb_status = irb_status
self.comment = comment
self.primary_investigator_id = primary_investigator_id self.primary_investigator_id = primary_investigator_id
self.sponsor = sponsor self.sponsor = sponsor
self.hsr_number = hsr_number self.hsr_number = hsr_number
@ -146,11 +167,13 @@ class Study(object):
self.warnings = [] self.warnings = []
self.files = files self.files = files
self.enrollment_date = enrollment_date self.enrollment_date = enrollment_date
self.events_history = events_history
@classmethod @classmethod
def from_model(cls, study_model: StudyModel): def from_model(cls, study_model: StudyModel):
id = study_model.id # Just read some value, in case the dict expired, otherwise dict may be empty. id = study_model.id # Just read some value, in case the dict expired, otherwise dict may be empty.
args = dict((k, v) for k, v in study_model.__dict__.items() if not k.startswith('_')) args = dict((k, v) for k, v in study_model.__dict__.items() if not k.startswith('_'))
args['events_history'] = study_model.events_history # For some reason this attribute is not picked up
instance = cls(**args) instance = cls(**args)
return instance return instance
@ -165,18 +188,15 @@ class Study(object):
if status == StudyStatus.open_for_enrollment: if status == StudyStatus.open_for_enrollment:
study_model.enrollment_date = self.enrollment_date study_model.enrollment_date = self.enrollment_date
# change = { study_event = StudyEvent(
# 'status': ProtocolBuilderStatus(self.protocol_builder_status).value, study=study_model,
# 'comment': '' if not hasattr(self, 'comment') else self.comment, status=status,
# 'date': str(datetime.datetime.now()) comment='' if not hasattr(self, 'comment') else self.comment,
# } event_type=StudyEventType.user,
user_uid=UserService.current_user().uid if UserService.has_user() else None,
# if study_model.changes_history: )
# changes_history = json.loads(study_model.changes_history) db.session.add(study_event)
# changes_history.append(change) db.session.commit()
# else:
# changes_history = [change]
# study_model.changes_history = json.dumps(changes_history)
def model_args(self): def model_args(self):
@ -207,6 +227,15 @@ class StudyForUpdateSchema(ma.Schema):
return Study(**data) return Study(**data)
class StudyEventSchema(ma.Schema):
id = fields.Integer(required=False)
create_date = fields.DateTime()
status = EnumField(StudyStatus, by_value=True)
comment = fields.String(allow_none=True)
event_type = EnumField(StudyEvent, by_value=True)
class StudySchema(ma.Schema): class StudySchema(ma.Schema):
id = fields.Integer(required=False, allow_none=True) id = fields.Integer(required=False, allow_none=True)
@ -220,11 +249,13 @@ class StudySchema(ma.Schema):
files = fields.List(fields.Nested(FileSchema), dump_only=True) files = fields.List(fields.Nested(FileSchema), dump_only=True)
approvals = fields.List(fields.Nested('ApprovalSchema'), dump_only=True) approvals = fields.List(fields.Nested('ApprovalSchema'), dump_only=True)
enrollment_date = fields.Date(allow_none=True) enrollment_date = fields.Date(allow_none=True)
events_history = fields.List(fields.Nested('StudyEventSchema'), dump_only=True)
class Meta: class Meta:
model = Study model = Study
additional = ["id", "title", "last_updated", "primary_investigator_id", "user_uid", additional = ["id", "title", "last_updated", "primary_investigator_id", "user_uid",
"sponsor", "ind_number", "approvals", "files", "enrollment_date"] "sponsor", "ind_number", "approvals", "files", "enrollment_date",
"events_history"]
unknown = INCLUDE unknown = INCLUDE
@marshmallow.post_load @marshmallow.post_load

View File

@ -12,7 +12,7 @@ from crc.api.common import ApiError
from crc.models.file import FileModel, FileModelSchema, File from crc.models.file import FileModel, FileModelSchema, File
from crc.models.ldap import LdapSchema from crc.models.ldap import LdapSchema
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata, StudyEvent
from crc.models.task_event import TaskEventModel, TaskEvent from crc.models.task_event import TaskEventModel, TaskEvent
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \ from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus WorkflowStatus
@ -53,6 +53,7 @@ class StudyService(object):
loading up and executing all the workflows in a study to calculate information.""" loading up and executing all the workflows in a study to calculate information."""
if not study_model: if not study_model:
study_model = session.query(StudyModel).filter_by(id=study_id).first() study_model = session.query(StudyModel).filter_by(id=study_id).first()
study = Study.from_model(study_model) study = Study.from_model(study_model)
study.categories = StudyService.get_categories() study.categories = StudyService.get_categories()
workflow_metas = StudyService.__get_workflow_metas(study_id) workflow_metas = StudyService.__get_workflow_metas(study_id)
@ -77,9 +78,11 @@ class StudyService(object):
@staticmethod @staticmethod
def delete_study(study_id): def delete_study(study_id):
session.query(TaskEventModel).filter_by(study_id=study_id).delete() session.query(TaskEventModel).filter_by(study_id=study_id).delete()
# session.query(StudyEvent).filter_by(study_id=study_id).delete()
for workflow in session.query(WorkflowModel).filter_by(study_id=study_id): for workflow in session.query(WorkflowModel).filter_by(study_id=study_id):
StudyService.delete_workflow(workflow) StudyService.delete_workflow(workflow)
session.query(StudyModel).filter_by(id=study_id).delete() study = session.query(StudyModel).filter_by(id=study_id).first()
session.delete(study)
session.commit() session.commit()
@staticmethod @staticmethod

View File

@ -0,0 +1,41 @@
"""empty message
Revision ID: 69081f1ff387
Revises: 1c3f88dbccc3
Create Date: 2020-08-12 09:58:36.886096
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '69081f1ff387'
down_revision = '1c3f88dbccc3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('study_event',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('study_id', sa.Integer(), nullable=False),
sa.Column('create_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('status', sa.Enum('in_progress', 'hold', 'open_for_enrollment', 'abandoned', name='studystatusenum'), nullable=True),
sa.Column('comment', sa.String(), nullable=True),
sa.Column('event_type', sa.Enum('user', 'automatic', name='studyeventtype'), nullable=True),
sa.Column('user_uid', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
sa.ForeignKeyConstraint(['user_uid'], ['user.uid'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('study_event')
op.execute('drop type studystatusenum')
op.execute('drop type studyeventtype')
# ### end Alembic commands ###

View File

@ -11,7 +11,7 @@ from crc.models.protocol_builder import ProtocolBuilderStatus, \
ProtocolBuilderStudySchema ProtocolBuilderStudySchema
from crc.models.approval import ApprovalStatus from crc.models.approval import ApprovalStatus
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel, StudySchema, StudyStatus from crc.models.study import StudyEvent, StudyModel, StudySchema, StudyStatus, StudyEventType
from crc.models.workflow import WorkflowSpecModel, WorkflowModel from crc.models.workflow import WorkflowSpecModel, WorkflowModel
from crc.services.file_service import FileService from crc.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
@ -134,10 +134,12 @@ class TestStudyApi(BaseTest):
def test_update_study(self): def test_update_study(self):
self.load_example_data() self.load_example_data()
update_comment = 'Updating the study'
study: StudyModel = session.query(StudyModel).first() study: StudyModel = session.query(StudyModel).first()
study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility" study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility"
study_schema = StudySchema().dump(study) study_schema = StudySchema().dump(study)
study_schema['status'] = StudyStatus.in_progress.value study_schema['status'] = StudyStatus.in_progress.value
study_schema['comment'] = update_comment
rv = self.app.put('/v1.0/study/%i' % study.id, rv = self.app.put('/v1.0/study/%i' % study.id,
content_type="application/json", content_type="application/json",
headers=self.logged_in_headers(), headers=self.logged_in_headers(),
@ -147,6 +149,13 @@ class TestStudyApi(BaseTest):
self.assertEqual(study.title, json_data['title']) self.assertEqual(study.title, json_data['title'])
self.assertEqual(study.status.value, json_data['status']) self.assertEqual(study.status.value, json_data['status'])
# Making sure events history is being properly recorded
study_event = session.query(StudyEvent).first()
self.assertIsNotNone(study_event)
self.assertEqual(study_event.status, StudyStatus.in_progress)
self.assertEqual(study_event.comment, update_comment)
self.assertEqual(study_event.user_uid, self.test_uid)
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies @patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
@ -245,8 +254,15 @@ class TestStudyApi(BaseTest):
def test_delete_study_with_workflow_and_status(self): def test_delete_study_with_workflow_and_status(self):
self.load_example_data() self.load_example_data()
workflow = session.query(WorkflowModel).first() workflow = session.query(WorkflowModel).first()
stats1 = StudyEvent(
study_id=workflow.study_id,
status=StudyStatus.in_progress,
comment='Some study status change event',
event_type=StudyEventType.user,
user_uid=self.users[0]['uid'],
)
stats2 = TaskEventModel(study_id=workflow.study_id, workflow_id=workflow.id, user_uid=self.users[0]['uid']) stats2 = TaskEventModel(study_id=workflow.study_id, workflow_id=workflow.id, user_uid=self.users[0]['uid'])
session.add(stats2) session.add_all([stats1, stats2])
session.commit() session.commit()
rv = self.app.delete('/v1.0/study/%i' % workflow.study_id, headers=self.logged_in_headers()) rv = self.app.delete('/v1.0/study/%i' % workflow.study_id, headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)

View File

@ -19,7 +19,7 @@ class TestStudyDetailsDocumentsScript(BaseTest):
""" """
1. get a list of all documents related to the study. 1. get a list of all documents related to the study.
2. For this study, is this document required accroding to the protocol builder? 2. For this study, is this document required accroding to the protocol builder?
3. For ALL uploaded documents, what the total number of files that were uploaded? per instance of this document naming 3. For ALL uploaded documents, what the total number of files that were uploaded? per instance of this document naming
convention that we are implementing for the IRB. convention that we are implementing for the IRB.
""" """

View File

@ -84,7 +84,7 @@ class TestStudyService(BaseTest):
# Assure the workflow is now started, and knows the total and completed tasks. # Assure the workflow is now started, and knows the total and completed tasks.
studies = StudyService.get_studies_for_user(user) studies = StudyService.get_studies_for_user(user)
workflow = next(iter(studies[0].categories[0].workflows)) # Workflows is a set. workflow = next(iter(studies[0].categories[0].workflows)) # Workflows is a set.
# self.assertEqual(WorkflowStatus.user_input_required, workflow.status) # self.assertEqual(WorkflowStatus.user_input_required, workflow.status)
self.assertTrue(workflow.total_tasks > 0) self.assertTrue(workflow.total_tasks > 0)
self.assertEqual(0, workflow.completed_tasks) self.assertEqual(0, workflow.completed_tasks)
self.assertIsNotNone(workflow.spec_version) self.assertIsNotNone(workflow.spec_version)