Merge branch 'more-study-statuses-562' into waiting-workflow-errors-566

This commit is contained in:
mike cullerton 2021-12-10 16:52:07 -05:00
commit acbb8898e4
13 changed files with 163 additions and 91 deletions

View File

@ -1,6 +1,4 @@
import datetime
import enum
import json
import marshmallow
from marshmallow import INCLUDE, fields
@ -9,13 +7,11 @@ from sqlalchemy import func
from crc import db, ma
from crc.api.common import ApiErrorSchema, ApiError
from crc.models.file import FileModel, SimpleFileSchema, FileSchema
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 WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \
WorkflowModel
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowModel
from crc.services.file_service import FileService
from crc.services.user_service import UserService
class StudyStatus(enum.Enum):
@ -23,6 +19,10 @@ class StudyStatus(enum.Enum):
hold = 'hold'
open_for_enrollment = 'open_for_enrollment'
abandoned = 'abandoned'
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'
@ -44,7 +44,6 @@ class StudyEventType(enum.Enum):
automatic = 'automatic'
class StudyModel(db.Model):
__tablename__ = 'study'
id = db.Column(db.Integer, primary_key=True)
@ -52,6 +51,7 @@ class StudyModel(db.Model):
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))
progress_status = db.Column(db.Enum(ProgressStatus))
irb_status = db.Column(db.Enum(IrbStatus))
primary_investigator_id = db.Column(db.String, nullable=True)
sponsor = db.Column(db.String, nullable=True)
@ -184,7 +184,7 @@ class CategorySchema(ma.Schema):
class Study(object):
def __init__(self, title, short_title, last_updated, primary_investigator_id, user_uid,
id=None, status=None, irb_status=None, short_name=None, proposal_name=None, comment="",
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=[],
last_activity_user="",last_activity_date =None,create_user_display="", **argsv):
@ -197,6 +197,7 @@ class Study(object):
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.primary_investigator_id = primary_investigator_id

View File

@ -1,13 +1,13 @@
import enum
import marshmallow
from marshmallow import EXCLUDE,fields
from marshmallow import EXCLUDE
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from sqlalchemy import func
from sqlalchemy.orm import backref
from crc import db
from crc.models.file import FileModel, FileDataModel
from crc.models.file import FileDataModel
class WorkflowSpecCategoryModel(db.Model):

View File

@ -0,0 +1,21 @@
from crc import session
from crc.models.study import StudyModel
from crc.scripts.script import Script
class GetStudyProgressStatus(Script):
def get_description(self):
return """
Get the progress status of the current study.
Progress status is only set when `status` is `in_progress`.
Progress status can be one of `in_progress`, `submitted_for_pre_review`, `in_pre_review`, `returned_from_pre_review`, `pre_review_complete`, `agenda_date_set`, `approved`, `approved_with_conditions`, `deferred`, or `disapproved`.
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
return self.do_task(task, study_id, workflow_id, *args, **kwargs)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
progress_status = session.query(StudyModel.progress_status).filter(StudyModel.id == study_id).scalar()
if progress_status:
return progress_status.value

View File

@ -1,14 +1,14 @@
from crc import session
from crc.api.common import ApiError
from crc.models.study import StudyModel, StudyStatus
from crc.models.study import StudyModel, ProgressStatus
from crc.scripts.script import Script
class SetStudyStatus(Script):
class SetStudyProgressStatus(Script):
def get_description(self):
return """Set the status of the current study.
Status can be one of `in_progress`, `hold`, `open_for_enrollment`, or `abandoned`."""
return """Set the progress status of the current study.
Progress status can be one of `in_progress`, `submitted_for_pre_review`, `in_pre_review`, `returned_from_pre_review`, `pre_review_complete`, `agenda_date_set`, `approved`, `approved_with_conditions`, `deferred`, or `disapproved`."""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
@ -19,17 +19,17 @@ class SetStudyStatus(Script):
new_status = args[0]
try:
study_status = getattr(StudyStatus, new_status)
progress_status = getattr(ProgressStatus, new_status)
except AttributeError as ae:
raise ApiError.from_task(code='invalid_argument',
message=f"We could not find a status matching `{new_status}`. Original message: {ae}",
task=task)
return study_status.value
return progress_status.value
else:
raise ApiError.from_task(code='missing_argument',
message='You must include the new status when calling `set_study_status` script. '
message='You must include the new status when calling `set_study_progress_status` script. '
'The new status must be one of `in_progress`, `hold`, `open_for_enrollment`, or `abandoned`.',
task=task)
@ -42,28 +42,26 @@ class SetStudyStatus(Script):
else:
new_status = args[0]
# Get StudyStatus object for new_status
# Get ProgressStatus object for new_status
try:
study_status = getattr(StudyStatus, new_status)
progress_status = getattr(ProgressStatus, new_status)
# Invalid argument
except AttributeError as ae:
raise ApiError.from_task(code='invalid_argument',
message=f"We could not find a status matching `{new_status}`. Original message: {ae}"
'The new status must be one of `in_progress`, `hold`, `open_for_enrollment`, or `abandoned`.',
message=f"We could not find a status matching `{new_status}`. Original message: {ae}.",
task=task)
# Set new status
study_model = session.query(StudyModel).filter(StudyModel.id == study_id).first()
study_model.status = study_status
study_model.progress_status = progress_status
session.commit()
return study_model.status.value
return study_model.progress_status.value
# Missing argument
else:
raise ApiError.from_task(code='missing_argument',
message='You must include the new status when calling `set_study_status` script. '
'The new status must be one of `in_progress`, `hold`, `open_for_enrollment`, or `abandoned`.',
message='You must include the new progress status when calling `set_study_progress_status` script. ',
task=task)

View File

@ -16,7 +16,7 @@ from crc.models.ldap import LdapSchema
from crc.models.protocol_builder import ProtocolBuilderCreatorStudy
from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata, StudyEventType, StudyEvent, \
StudyAssociated
StudyAssociated, ProgressStatus
from crc.models.task_event import TaskEventModel
from crc.models.task_log import TaskLogModel
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
@ -382,11 +382,14 @@ class StudyService(object):
# has a reference to every available workflow (though some may not have started yet)
for pb_study in pb_studies:
new_status = None
new_progress_status = None
db_study = next((s for s in db_studies if s.id == pb_study.STUDYID), None)
if not db_study:
db_study = StudyModel(id=pb_study.STUDYID)
db_study.status = None # Force a new sa
new_status = StudyStatus.in_progress
new_progress_status = ProgressStatus.in_progress
session.add(db_study)
db_studies.append(db_study)
@ -396,6 +399,9 @@ class StudyService(object):
# If there is a new automatic status change and there isn't a manual change in place, record it.
if new_status and db_study.status != StudyStatus.hold:
db_study.status = new_status
# make sure status is `in_progress`, before processing new automatic progress_status.
if new_progress_status and db_study.status == StudyStatus.in_progress:
db_study.progress_status = new_progress_status
StudyService.add_study_update_event(db_study,
status=new_status,
event_type=StudyEventType.automatic)

View File

@ -1,9 +1,7 @@
import copy
import json
import string
import sys
import traceback
from datetime import datetime
import random
import string
from datetime import datetime
@ -14,26 +12,22 @@ from SpiffWorkflow import Task as SpiffTask, WorkflowException, NavItem
from SpiffWorkflow.bpmn.PythonScriptEngine import Box
from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.MultiInstanceTask import MultiInstanceTask
from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask
from SpiffWorkflow.bpmn.specs.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.specs import CancelTask, StartTask, MultiChoice
from SpiffWorkflow.specs import CancelTask, StartTask
from SpiffWorkflow.util.deep_merge import DeepMerge
from SpiffWorkflow.util.metrics import timeit
from jinja2 import Template
from crc import db, app, session, connexion_app
from crc import db, app, session
from crc.api.common import ApiError
from crc.models.api_models import Task, MultiInstanceType, WorkflowApi
from crc.models.data_store import DataStoreModel
from crc.models.file import LookupDataModel, FileModel, File, FileSchema
from crc.models.ldap import LdapModel
from crc.models.study import StudyModel
from crc.models.task_event import TaskEventModel
from crc.models.user import UserModel, UserModelSchema
from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel, WorkflowStatus, WorkflowSpecModel, WorkflowSpecCategoryModel
from crc.services.data_store_service import DataStoreBase

View File

@ -0,0 +1,28 @@
"""new study progress statuses
Revision ID: d830959e96c0
Revises: a4f87f90cc64
Create Date: 2021-12-09 11:55:28.890437
"""
from alembic import op
import sqlalchemy as sa
from crc.models.study import StudyStatus
# revision identifiers, used by Alembic.
revision = 'd830959e96c0'
down_revision = 'a4f87f90cc64'
branch_labels = None
depends_on = None
def upgrade():
op.execute("CREATE TYPE progressstatus AS ENUM('in_progress', 'submitted_for_pre_review', 'in_pre_review', 'returned_from_pre_review', 'pre_review_complete', 'agenda_date_set', 'approved', 'approved_with_conditions', 'deferred', 'disapproved')")
op.add_column('study', sa.Column('progress_status', sa.Enum('in_progress', 'submitted_for_pre_review', 'in_pre_review', 'returned_from_pre_review', 'pre_review_complete', 'agenda_date_set', 'approved', 'approved_with_conditions', 'deferred', 'disapproved', name='progressstatus'), nullable=True))
op.execute("update study set progress_status = 'in_progress' where status='in_progress'")
def downgrade():
op.drop_column('study', 'progress_status')
op.execute('DROP TYPE progressstatus')

View File

@ -1,41 +0,0 @@
"""new study statuses
Revision ID: d830959e96c0
Revises: a4f87f90cc64
Create Date: 2021-12-09 11:55:28.890437
"""
from alembic import op
import sqlalchemy as sa
from crc.models.study import StudyStatus
# revision identifiers, used by Alembic.
revision = 'd830959e96c0'
down_revision = 'a4f87f90cc64'
branch_labels = None
depends_on = None
def upgrade():
op.execute("ALTER TYPE StudyStatus ADD VALUE 'submitted_for_pre_review'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'in_pre_review'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'returned_from_pre_review'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'pre_review_complete'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'agenda_date_set'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'approved'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'approved_with_conditions'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'deferred'")
op.execute("ALTER TYPE StudyStatus ADD VALUE 'disapproved'")
def downgrade():
op.execute("UPDATE study set status=null WHERE status in ("
"'submitted_for_pre_review', 'in_pre_review', 'returned_from_pre_review', "
"'pre_review_complete', 'agenda_date_set', 'approved', 'approved_with_conditions', "
"'deferred', 'disapproved')")
op.execute('ALTER TYPE StudyStatus RENAME TO ss_old;')
op.execute("CREATE TYPE StudyStatus AS ENUM('in_progress', 'hold', 'open_for_enrollment', 'abandoned')")
op.execute("ALTER TABLE study ALTER COLUMN status TYPE studystatus USING status::text::studystatus;")
op.execute("update study set status = 'in_progress' where status is null")
op.execute('DROP TYPE ss_old;')

View File

@ -15,7 +15,7 @@ from crc import app, db, session
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel, StudyStatus
from crc.models.study import StudyModel, StudyStatus, ProgressStatus
from crc.models.ldap import LdapModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecCategoryModel
@ -61,6 +61,7 @@ class BaseTest(unittest.TestCase):
'title': 'The impact of fried pickles on beer consumption in bipedal software developers.',
'last_updated': datetime.datetime.utcnow(),
'status': StudyStatus.in_progress,
'progress_status': ProgressStatus.in_progress,
'primary_investigator_id': 'dhf8r',
'sponsor': 'Sartography Pharmaceuticals',
'ind_number': '1234',
@ -71,6 +72,7 @@ class BaseTest(unittest.TestCase):
'title': 'Requirement of hippocampal neurogenesis for the behavioral effects of soft pretzels',
'last_updated': datetime.datetime.utcnow(),
'status': StudyStatus.in_progress,
'progress_status': ProgressStatus.in_progress,
'primary_investigator_id': 'dhf8r',
'sponsor': 'Makerspace & Co.',
'ind_number': '5678',

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1mi5jsa" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_StudyProgressStatus" name="Study Progress Status" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" name="Start">
<bpmn:outgoing>Flow_1iqprcz</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1iqprcz" sourceRef="StartEvent_1" targetRef="Activity_GetStudyProgressStatus" />
<bpmn:scriptTask id="Activity_GetStudyProgressStatus" name="Get Study Progress Status">
<bpmn:incoming>Flow_1iqprcz</bpmn:incoming>
<bpmn:outgoing>Flow_0npc38l</bpmn:outgoing>
<bpmn:script>study_progress_status = get_study_progress_status()</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_005hgvx" name="Display Study Status">
<bpmn:documentation># Study Progress Status
{{ study_progress_status }}</bpmn:documentation>
<bpmn:incoming>Flow_0npc38l</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0npc38l" sourceRef="Activity_GetStudyProgressStatus" targetRef="Event_005hgvx" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_StudyProgressStatus">
<bpmndi:BPMNEdge id="Flow_0npc38l_di" bpmnElement="Flow_0npc38l">
<di:waypoint x="370" y="117" />
<di:waypoint x="432" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1iqprcz_di" bpmnElement="Flow_1iqprcz">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="186" y="142" width="24" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_07bvb7w_di" bpmnElement="Activity_GetStudyProgressStatus">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_005hgvx_di" bpmnElement="Event_005hgvx">
<dc:Bounds x="432" y="99" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="417" y="142" width="68" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -9,9 +9,6 @@
<camunda:formData>
<camunda:formField id="selected_status" label="Select Study Status" type="enum">
<camunda:value id="in_progress" name="In Progress" />
<camunda:value id="hold" name="Hold" />
<camunda:value id="open_for_enrollment" name="Open For Enrollment" />
<camunda:value id="abandoned" name="Abandoned" />
<camunda:value id="approved" name="Approved" />
<camunda:value id="disapproved" name="Disapproved" />
<camunda:value id="asdf" name="asdf" />
@ -25,7 +22,7 @@
<bpmn:scriptTask id="Activity_SetSelectedStatus" name="Set Selected Status">
<bpmn:incoming>Flow_0q0rtvj</bpmn:incoming>
<bpmn:outgoing>Flow_0ana8xt</bpmn:outgoing>
<bpmn:script>returned_status = set_study_status(selected_status)</bpmn:script>
<bpmn:script>returned_status = set_study_progress_status(selected_status)</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0ana8xt" sourceRef="Activity_SetSelectedStatus" targetRef="Activity_GetNewStatus" />
<bpmn:manualTask id="Activity_DisplayStatus" name="Display Status">
@ -55,13 +52,13 @@
<bpmn:scriptTask id="Activity_GetOriginalStatus" name="Get Original Status">
<bpmn:incoming>Flow_0c77bdh</bpmn:incoming>
<bpmn:outgoing>Flow_1e9oiuw</bpmn:outgoing>
<bpmn:script>original_status = get_study_status()</bpmn:script>
<bpmn:script>original_status = get_study_progress_status()</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0nckhhn" sourceRef="Activity_GetNewStatus" targetRef="Activity_DisplayStatus" />
<bpmn:scriptTask id="Activity_GetNewStatus" name="Get New Status">
<bpmn:incoming>Flow_0ana8xt</bpmn:incoming>
<bpmn:outgoing>Flow_0nckhhn</bpmn:outgoing>
<bpmn:script>new_status = get_study_status()</bpmn:script>
<bpmn:script>new_status = get_study_progress_status()</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">

View File

@ -0,0 +1,16 @@
from tests.base_test import BaseTest
from crc import session
from crc.models.study import StudyModel, ProgressStatus
class TestGetStudyProgressStatus(BaseTest):
def test_get_study_progress_status(self):
workflow = self.create_workflow('get_study_progress_status')
study_model = session.query(StudyModel).filter(StudyModel.id == workflow.study_id).first()
study_model.progress_status = ProgressStatus.approved
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
self.assertEqual(task.data['study_progress_status'], workflow.study.progress_status.value)

View File

@ -1,14 +1,16 @@
from tests.base_test import BaseTest
from crc.models.study import ProgressStatus
class TestSetStudyStatus(BaseTest):
def test_set_study_status_validation(self):
class TestSetStudyProgressStatus(BaseTest):
def test_set_study_progress_status_validation(self):
self.load_example_data()
spec_model = self.load_test_spec('set_study_status')
spec_model = self.load_test_spec('set_study_progress_status')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
# The workflow has an enum option that causes an exception.
# We take advantage of this in test_set_study_status_fail below.
# We take advantage of this in test_set_study_progress_status_fail below.
# Sometimes, the validation process chooses the failing path,
# so we have to check for that here.
try:
@ -17,8 +19,9 @@ class TestSetStudyStatus(BaseTest):
# 'asdf' is the failing enum option
self.assertEqual('asdf', rv.json[0]['task_data']['selected_status'])
def test_set_study_status(self):
workflow = self.create_workflow('set_study_status')
def test_set_study_progress_status(self):
workflow = self.create_workflow('set_study_progress_status')
workflow.study.progress_status = ProgressStatus.in_progress
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
@ -32,10 +35,10 @@ class TestSetStudyStatus(BaseTest):
self.assertEqual('disapproved', task.data['selected_status'])
self.assertEqual('disapproved', task.data['new_status'])
def test_set_study_status_fail(self):
def test_set_study_progress_status_fail(self):
self.load_example_data()
workflow = self.create_workflow('set_study_status')
workflow = self.create_workflow('set_study_progress_status')
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task