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.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \
WorkflowModel
from crc.services.user_service import UserService
class StudyStatus(enum.Enum):
@ -28,6 +29,11 @@ class IrbStatus(enum.Enum):
hsr_assigned = 'hsr number assigned'
class StudyEventType(enum.Enum):
user = 'user'
automatic = 'automatic'
class StudyModel(db.Model):
__tablename__ = 'study'
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)
on_hold = db.Column(db.Boolean, default=False)
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):
self.hsr_number = pbs.HSRNUMBER
@ -60,6 +68,18 @@ class StudyModel(db.Model):
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):
def __init__(self, id, name = None, display_name = None, description = None, spec_version = None,
category_id = None, category_display_name = None, state: WorkflowState = None,
@ -128,15 +148,16 @@ class CategorySchema(ma.Schema):
class Study(object):
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=[],
files=[], approvals=[], enrollment_date=None, **argsv):
files=[], approvals=[], enrollment_date=None, events_history=[], **argsv):
self.id = id
self.user_uid = user_uid
self.title = title
self.last_updated = last_updated
self.status = status
self.irb_status = irb_status
self.comment = comment
self.primary_investigator_id = primary_investigator_id
self.sponsor = sponsor
self.hsr_number = hsr_number
@ -146,11 +167,13 @@ class Study(object):
self.warnings = []
self.files = files
self.enrollment_date = enrollment_date
self.events_history = events_history
@classmethod
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.
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)
return instance
@ -165,18 +188,15 @@ class Study(object):
if status == StudyStatus.open_for_enrollment:
study_model.enrollment_date = self.enrollment_date
# change = {
# 'status': ProtocolBuilderStatus(self.protocol_builder_status).value,
# 'comment': '' if not hasattr(self, 'comment') else self.comment,
# 'date': str(datetime.datetime.now())
# }
# if study_model.changes_history:
# changes_history = json.loads(study_model.changes_history)
# changes_history.append(change)
# else:
# changes_history = [change]
# study_model.changes_history = json.dumps(changes_history)
study_event = StudyEvent(
study=study_model,
status=status,
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,
)
db.session.add(study_event)
db.session.commit()
def model_args(self):
@ -207,6 +227,15 @@ class StudyForUpdateSchema(ma.Schema):
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):
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)
approvals = fields.List(fields.Nested('ApprovalSchema'), dump_only=True)
enrollment_date = fields.Date(allow_none=True)
events_history = fields.List(fields.Nested('StudyEventSchema'), dump_only=True)
class Meta:
model = Study
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
@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.ldap import LdapSchema
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.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus
@ -53,6 +53,7 @@ class StudyService(object):
loading up and executing all the workflows in a study to calculate information."""
if not study_model:
study_model = session.query(StudyModel).filter_by(id=study_id).first()
study = Study.from_model(study_model)
study.categories = StudyService.get_categories()
workflow_metas = StudyService.__get_workflow_metas(study_id)
@ -77,9 +78,11 @@ class StudyService(object):
@staticmethod
def delete_study(study_id):
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):
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()
@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
from crc.models.approval import ApprovalStatus
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.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor
@ -134,10 +134,12 @@ class TestStudyApi(BaseTest):
def test_update_study(self):
self.load_example_data()
update_comment = 'Updating the study'
study: StudyModel = session.query(StudyModel).first()
study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility"
study_schema = StudySchema().dump(study)
study_schema['status'] = StudyStatus.in_progress.value
study_schema['comment'] = update_comment
rv = self.app.put('/v1.0/study/%i' % study.id,
content_type="application/json",
headers=self.logged_in_headers(),
@ -147,6 +149,13 @@ class TestStudyApi(BaseTest):
self.assertEqual(study.title, json_data['title'])
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_required_docs') # mock_docs
@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):
self.load_example_data()
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'])
session.add(stats2)
session.add_all([stats1, stats2])
session.commit()
rv = self.app.delete('/v1.0/study/%i' % workflow.study_id, headers=self.logged_in_headers())
self.assert_success(rv)

View File

@ -84,7 +84,7 @@ class TestStudyService(BaseTest):
# Assure the workflow is now started, and knows the total and completed tasks.
studies = StudyService.get_studies_for_user(user)
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.assertEqual(0, workflow.completed_tasks)
self.assertIsNotNone(workflow.spec_version)