cr-connect-workflow/crc/models/study.py

320 lines
12 KiB
Python

import enum
import marshmallow
from marshmallow import INCLUDE, fields
from marshmallow_enum import EnumField
from sqlalchemy import func
from crc import db, ma
from crc.api.common import ApiErrorSchema, ApiError
from crc.models.file import FileSchema
from crc.models.ldap import LdapModel, LdapSchema
from crc.models.protocol_builder import ProtocolBuilderCreatorStudy
from crc.models.workflow import WorkflowSpecCategory, WorkflowState, WorkflowStatus, WorkflowModel, WorkflowSpecInfo
class StudyStatus(enum.Enum):
in_progress = 'in_progress'
hold = 'hold'
open_for_enrollment = 'open_for_enrollment'
abandoned = 'abandoned'
cr_connect_complete = 'cr_connect_complete'
class ProgressStatus(enum.Enum):
in_progress = 'in_progress'
submitted_for_pre_review = 'submitted_for_pre_review'
in_pre_review = 'in_pre_review'
returned_from_pre_review = 'returned_from_pre_review'
pre_review_complete = 'pre_review_complete'
agenda_date_set = 'agenda_date_set'
approved = 'approved'
approved_with_conditions = 'approved_with_conditions'
deferred = 'deferred'
disapproved = 'disapproved'
ready_for_pre_review = 'ready_for_pre_review'
resubmitted_for_pre_review = 'resubmitted_for_pre_review'
class IrbStatus(enum.Enum):
incomplete_in_protocol_builder = 'incomplete in protocol builder'
completed_in_protocol_builder = 'completed in protocol builder'
class StudyEventType(enum.Enum):
user = 'user'
automatic = 'automatic'
class StudyModel(db.Model):
__tablename__ = 'study'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
short_title = db.Column(db.String, nullable=True)
last_updated = db.Column(db.DateTime(timezone=True), server_default=func.now())
status = db.Column(db.Enum(StudyStatus))
irb_status = db.Column(db.Enum(IrbStatus))
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=False)
enrollment_date = db.Column(db.DateTime(timezone=True), nullable=True)
review_type = db.Column(db.Integer, nullable=True) # Nullable only because we are adding it late, always set in practice.
events_history = db.relationship("StudyEvent", cascade="all, delete, delete-orphan")
primary_investigator = db.Column(db.String, nullable=True)
#fixme: All of the fields below should likely be removed.
#events = db.relationship("TaskEventModel")
progress_status = db.Column(db.Enum(ProgressStatus))
sponsor = db.Column(db.String, nullable=True)
ind_number = db.Column(db.String, nullable=True)
investigator_uids = db.Column(db.ARRAY(db.String), nullable=True)
requirements = db.Column(db.ARRAY(db.Integer), nullable=True)
on_hold = db.Column(db.Boolean, default=False)
proposal_name = db.Column(db.String, nullable=True)
short_name = db.Column(db.String, nullable=True)
progress = db.Column(db.Integer, nullable=True)
def update_from_protocol_builder(self, study: ProtocolBuilderCreatorStudy, user_id):
self.title = study.TITLE
self.user_uid = user_id
self.review_type = study.REVIEW_TYPE
if study.DATELASTMODIFIED:
self.last_updated = study.DATELASTMODIFIED
else:
self.last_updated = study.DATECREATED
self.irb_status = IrbStatus.incomplete_in_protocol_builder
class StudyAssociated(db.Model):
"""
This model allows us to associate people with a study, and optionally
give them edit access. This allows us to create a table with PI, D_CH, etc.
and give access to people other than the study owner.
Task_Events will still work as they have previously
"""
__tablename__ = 'study_associated_user'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False)
uid = db.Column(db.String, db.ForeignKey('ldap_model.uid'), nullable=False)
role = db.Column(db.String, nullable=True)
send_email = db.Column(db.Boolean, nullable=True)
access = db.Column(db.Boolean, nullable=True)
ldap_info = db.relationship(LdapModel)
class StudyAssociatedSchema(ma.Schema):
class Meta:
fields = ['uid', 'role', 'send_email', 'access', 'ldap_info']
model = StudyAssociated
unknown = INCLUDE
ldap_info = fields.Nested(LdapSchema, dump_only=True)
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), server_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 CategoryMetadata(object):
def __init__(self, id=None, state: WorkflowState = None, message=None):
self.id = id
self.state = state
self.message = message
class CategoryMetadataSchema(ma.Schema):
state = EnumField(WorkflowState)
class Meta:
model = CategoryMetadata
additional = ["id", "message"]
unknown = INCLUDE
class WorkflowMetadata(object):
def __init__(self, id, display_name=None, description=None, spec_version=None,
category_id=None, category_display_name=None, state: WorkflowState = None,
status: WorkflowStatus = None, total_tasks=None, completed_tasks=None,
is_review=None, display_order=None, state_message=None, workflow_spec_id=None):
self.id = id
self.display_name = display_name
self.description = description
self.spec_version = spec_version
self.category_id = category_id
self.category_display_name = category_display_name
self.state = state
self.state_message = state_message
self.status = status
self.total_tasks = total_tasks
self.completed_tasks = completed_tasks
self.is_review = is_review
self.display_order = display_order
self.workflow_spec_id = workflow_spec_id
@classmethod
def from_workflow(cls, workflow: WorkflowModel, spec: WorkflowSpecInfo):
instance = cls(
id=workflow.id,
display_name=spec.display_name,
description=spec.description,
category_id=spec.category_id,
category_display_name=spec.category.display_name,
state=WorkflowState.optional,
status=workflow.status,
total_tasks=workflow.total_tasks,
completed_tasks=workflow.completed_tasks,
is_review=spec.is_review,
display_order=spec.display_order,
workflow_spec_id=workflow.workflow_spec_id
)
return instance
class WorkflowMetadataSchema(ma.Schema):
state = EnumField(WorkflowState)
status = EnumField(WorkflowStatus)
class Meta:
model = WorkflowMetadata
additional = ["id", "display_name", "description",
"total_tasks", "completed_tasks", "display_order",
"category_id", "is_review", "category_display_name", "state_message"]
unknown = INCLUDE
class Category(object):
def __init__(self, model: WorkflowSpecCategory):
self.id = model.id
self.display_name = model.display_name
self.display_order = model.display_order
self.admin = model.admin
class CategorySchema(ma.Schema):
workflows = fields.List(fields.Nested(WorkflowMetadataSchema), dump_only=True)
meta = fields.Nested(CategoryMetadataSchema)
class Meta:
model = Category
additional = ["id", "display_name", "display_order", "admin"]
unknown = INCLUDE
class Study(object):
def __init__(self, title, short_title, last_updated, user_uid,
id=None, status=None, progress_status=None, irb_status=None, short_name=None, proposal_name=None,
comment="",
sponsor="", ind_number="", categories=[],
files=[], approvals=[], enrollment_date=None, events_history=[],
primary_investigator="",
last_activity_user="",last_activity_date =None,create_user_display="", progress=0, **argsv):
self.id = id
self.user_uid = user_uid
self.create_user_display = create_user_display
self.last_activity_date = last_activity_date
self.last_activity_user = last_activity_user
self.title = title
self.short_title = short_title
self.last_updated = last_updated
self.status = status
self.progress_status = progress_status
self.irb_status = irb_status
self.comment = comment
self.sponsor = sponsor
self.ind_number = ind_number
self.categories = categories
self.approvals = approvals
self.warnings = []
self.files = files
self.enrollment_date = enrollment_date
self.events_history = events_history
self.short_name = short_name
self.proposal_name = proposal_name
self.primary_investigator = primary_investigator
self.progress = progress
@classmethod
def from_model(cls, study_model: StudyModel):
if study_model is not None and len(study_model.__dict__.items()) > 0:
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
else:
raise ApiError(code='empty_study_model',
message='There was a problem retrieving your study. StudyModel is empty.')
def model_args(self):
"""Arguments that can be passed into the Study Model to update it."""
self_dict = self.__dict__.copy()
del self_dict["categories"]
del self_dict["warnings"]
return self_dict
class StudyForUpdateSchema(ma.Schema):
id = fields.Integer(required=False, allow_none=True)
status = EnumField(StudyStatus, by_value=True)
sponsor = fields.String(allow_none=True)
ind_number = fields.String(allow_none=True)
enrollment_date = fields.DateTime(allow_none=True)
comment = fields.String(allow_none=True)
class Meta:
model = Study
unknown = INCLUDE
@marshmallow.post_load
def make_study(self, data, **kwargs):
"""Can load the basic study data for updates to the database, but categories are write only"""
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)
categories = fields.List(fields.Nested(CategorySchema), dump_only=True)
warnings = fields.List(fields.Nested(ApiErrorSchema), dump_only=True)
protocol_builder_status = EnumField(StudyStatus, by_value=True)
status = EnumField(StudyStatus, by_value=True)
progress_status = EnumField(ProgressStatus, by_value=True, allow_none=True)
short_title = fields.String(allow_none=True)
sponsor = fields.String(allow_none=True)
ind_number = fields.String(allow_none=True)
files = fields.List(fields.Nested(FileSchema), dump_only=True)
enrollment_date = fields.Date(allow_none=True)
events_history = fields.List(fields.Nested('StudyEventSchema'), dump_only=True)
short_name = fields.String(allow_none=True)
proposal_name = fields.String(allow_none=True)
review_type = fields.Integer(allow_none=True)
primary_investigator = fields.String(allow_none=True)
progress = fields.Integer(allow_none=True)
class Meta:
model = Study
additional = ["id", "title", "short_title", "last_updated", "user_uid",
"sponsor", "ind_number", "files", "enrollment_date",
"create_user_display", "last_activity_date", "last_activity_user",
"events_history", "short_name", "proposal_name", "progress", "primary_investigator"]
unknown = INCLUDE
@marshmallow.post_load
def make_study(self, data, **kwargs):
"""Can load the basic study data for updates to the database, but categories are write only"""
return Study(**data)