diff --git a/crc/models/study.py b/crc/models/study.py index be6d20dd..5848dfc6 100644 --- a/crc/models/study.py +++ b/crc/models/study.py @@ -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 @@ -265,6 +266,7 @@ class StudySchema(ma.Schema): 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) diff --git a/crc/models/workflow.py b/crc/models/workflow.py index ca848cf7..ced02aac 100644 --- a/crc/models/workflow.py +++ b/crc/models/workflow.py @@ -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): diff --git a/crc/scripts/get_study_progress_status.py b/crc/scripts/get_study_progress_status.py new file mode 100644 index 00000000..d2d6d2ee --- /dev/null +++ b/crc/scripts/get_study_progress_status.py @@ -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 diff --git a/crc/scripts/set_study_status.py b/crc/scripts/set_study_progress_status.py similarity index 66% rename from crc/scripts/set_study_status.py rename to crc/scripts/set_study_progress_status.py index 32397d0c..fb706f89 100644 --- a/crc/scripts/set_study_status.py +++ b/crc/scripts/set_study_progress_status.py @@ -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) diff --git a/crc/services/study_service.py b/crc/services/study_service.py index a5bd2d9e..4f720548 100755 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -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) diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index 8cd56c37..838d10c2 100755 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -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 diff --git a/migrations/versions/d830959e96c0_new_study_progress_statuses.py b/migrations/versions/d830959e96c0_new_study_progress_statuses.py new file mode 100644 index 00000000..244924b7 --- /dev/null +++ b/migrations/versions/d830959e96c0_new_study_progress_statuses.py @@ -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') diff --git a/tests/base_test.py b/tests/base_test.py index b162ae15..fe227ec1 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -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', diff --git a/tests/data/get_study_progress_status/get_study_progress_status.bpmn b/tests/data/get_study_progress_status/get_study_progress_status.bpmn new file mode 100644 index 00000000..a3d67fb8 --- /dev/null +++ b/tests/data/get_study_progress_status/get_study_progress_status.bpmn @@ -0,0 +1,47 @@ + + + + + Flow_1iqprcz + + + + Flow_1iqprcz + Flow_0npc38l + study_progress_status = get_study_progress_status() + + + # Study Progress Status +{{ study_progress_status }} + Flow_0npc38l + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/set_study_status/set_study_status.bpmn b/tests/data/set_study_progress_status/set_study_progress_status.bpmn similarity index 93% rename from tests/data/set_study_status/set_study_status.bpmn rename to tests/data/set_study_progress_status/set_study_progress_status.bpmn index af1027d3..65dd3c53 100644 --- a/tests/data/set_study_status/set_study_status.bpmn +++ b/tests/data/set_study_progress_status/set_study_progress_status.bpmn @@ -9,9 +9,8 @@ - - - + + @@ -23,7 +22,7 @@ Flow_0q0rtvj Flow_0ana8xt - returned_status = set_study_status(selected_status) + returned_status = set_study_progress_status(selected_status) @@ -53,13 +52,13 @@ Flow_0c77bdh Flow_1e9oiuw - original_status = get_study_status() + original_status = get_study_progress_status() Flow_0ana8xt Flow_0nckhhn - new_status = get_study_status() + new_status = get_study_progress_status() diff --git a/tests/scripts/test_get_study_progress_status.py b/tests/scripts/test_get_study_progress_status.py new file mode 100644 index 00000000..4fb2e333 --- /dev/null +++ b/tests/scripts/test_get_study_progress_status.py @@ -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) diff --git a/tests/scripts/test_set_study_progress_status.py b/tests/scripts/test_set_study_progress_status.py new file mode 100644 index 00000000..0b05b54e --- /dev/null +++ b/tests/scripts/test_set_study_progress_status.py @@ -0,0 +1,46 @@ +from tests.base_test import BaseTest + +from crc.models.study import ProgressStatus + + +class TestSetStudyProgressStatus(BaseTest): + + def test_set_study_progress_status_validation(self): + self.load_example_data() + 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_progress_status_fail below. + # Sometimes, the validation process chooses the failing path, + # so we have to check for that here. + try: + self.assertEqual([], rv.json) + except AssertionError: + # 'asdf' is the failing enum option + self.assertEqual('asdf', rv.json[0]['task_data']['selected_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 + + original_status = task.data['original_status'] + self.assertEqual('in_progress', original_status) + + workflow_api = self.complete_form(workflow, task, {'selected_status': 'disapproved'}) + task = workflow_api.next_task + + self.assertEqual('Activity_DisplayStatus', task.name) + self.assertEqual('disapproved', task.data['selected_status']) + self.assertEqual('disapproved', task.data['new_status']) + + def test_set_study_progress_status_fail(self): + + self.load_example_data() + workflow = self.create_workflow('set_study_progress_status') + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + with self.assertRaises(AssertionError): + self.complete_form(workflow, task, {'selected_status': 'asdf'}) diff --git a/tests/scripts/test_set_study_status.py b/tests/scripts/test_set_study_status.py deleted file mode 100644 index ece8c30c..00000000 --- a/tests/scripts/test_set_study_status.py +++ /dev/null @@ -1,35 +0,0 @@ -from tests.base_test import BaseTest - - -class TestSetStudyStatus(BaseTest): - - def test_set_study_status_validation(self): - self.load_example_data() - spec_model = self.load_test_spec('set_study_status') - rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers()) - self.assertEqual([], rv.json) - - def test_set_study_status(self): - workflow = self.create_workflow('set_study_status') - workflow_api = self.get_workflow_api(workflow) - task = workflow_api.next_task - - original_status = task.data['original_status'] - self.assertEqual('in_progress', original_status) - - workflow_api = self.complete_form(workflow, task, {'selected_status': 'hold'}) - task = workflow_api.next_task - - self.assertEqual('Activity_DisplayStatus', task.name) - self.assertEqual('hold', task.data['selected_status']) - self.assertEqual('hold', task.data['new_status']) - - def test_set_study_status_fail(self): - - self.load_example_data() - workflow = self.create_workflow('set_study_status') - workflow_api = self.get_workflow_api(workflow) - task = workflow_api.next_task - - with self.assertRaises(AssertionError): - self.complete_form(workflow, task, {'selected_status': 'asdf'})