From bdd07685c6819e30e9591dccebae5f7c2b19535d Mon Sep 17 00:00:00 2001 From: Aaron Louie Date: Sun, 15 Mar 2020 15:54:13 -0400 Subject: [PATCH] Adds status spec when adding a study, and adds/removes workflows from study based on output data from status spec. --- crc/api/study.py | 74 +++++++++++++-- crc/api/workflow.py | 16 ++-- example_data.py | 6 +- ...2_training_session_data_security_plan.dmn} | 15 +++- ...crc2_training_session_enter_core_info.dmn} | 4 +- ...aining_session_sponsor_funding_source.dmn} | 4 +- tests/data/status/status.bpmn | 89 +++++++------------ tests/data/status/two_forms.dmn | 30 ------- tests/test_study_api.py | 58 +++++++++++- tests/test_workflow_processor.py | 8 +- 10 files changed, 191 insertions(+), 113 deletions(-) rename tests/data/status/{pb_responses.dmn => crc2_training_session_data_security_plan.dmn} (64%) rename tests/data/status/{random_fact.dmn => crc2_training_session_enter_core_info.dmn} (81%) rename tests/data/status/{study_details.dmn => crc2_training_session_sponsor_funding_source.dmn} (79%) delete mode 100644 tests/data/status/two_forms.dmn diff --git a/crc/api/study.py b/crc/api/study.py index 9a77c36d..9fe48558 100644 --- a/crc/api/study.py +++ b/crc/api/study.py @@ -23,16 +23,46 @@ def all_studies(): @auth.login_required def add_study(body): - study = StudyModelSchema().load(body, session=session) + study: StudyModel = StudyModelSchema().load(body, session=session) + status_spec = __get_status_spec(study.status_spec_id) + + # Get latest status spec version + if status_spec is not None: + study.status_spec_id = status_spec.id + study.status_spec_version = WorkflowProcessor.get_latest_version_string(status_spec.id) + session.add(study) session.commit() - # FIXME: We need to ask the protocol builder what workflows to add to the study, not just add them all. - for spec in session.query(WorkflowSpecModel).all(): - WorkflowProcessor.create(study.id, spec.id) + __add_study_workflows_from_status(study.id, status_spec) return StudyModelSchema().dump(study) +def __get_status_spec(status_spec_id): + if status_spec_id is None: + return session.query(WorkflowSpecModel).filter_by(is_status=True).first() + else: + return session.query(WorkflowSpecModel).filter_by(id=status_spec_id).first() + + +def __add_study_workflows_from_status(study_id, status_spec): + all_specs = session.query(WorkflowSpecModel).all() + if status_spec is not None: + # Run status spec to get list of workflow specs applicable to this study + status_processor = WorkflowProcessor.create(study_id, status_spec) + status_processor.do_engine_steps() + status_data = status_processor.next_task().data + + # Only add workflow specs listed in status spec + for spec in all_specs: + if spec.id in status_data and status_data[spec.id]: + WorkflowProcessor.create(study_id, spec.id) + else: + # No status spec. Just add all workflows. + for spec in all_specs: + WorkflowProcessor.create(study_id, spec.id) + + @auth.login_required def update_study(study_id, body): if study_id is None: @@ -130,12 +160,43 @@ def post_update_study_from_protocol_builder(study_id): @auth.login_required def get_study_workflows(study_id): - workflow_models = session.query(WorkflowModel).filter_by(study_id=study_id).all() + + # Get study + study: StudyModel = session.query(StudyModel).filter_by(id=study_id).first() + + # Get study status spec + status_spec: WorkflowSpecModel = session.query(WorkflowSpecModel)\ + .filter_by(is_status=True).first() + + status_data = None + + if status_spec is not None: + # Run status spec + status_workflow_model: WorkflowModel = session.query(WorkflowModel)\ + .filter_by(study_id=study.id)\ + .filter_by(workflow_spec_id=status_spec.id)\ + .first() + status_processor = WorkflowProcessor(status_workflow_model) + + # Get list of active workflow specs for study + status_processor.do_engine_steps() + status_data = status_processor.bpmn_workflow.last_task.data + + # Get study workflows + workflow_models = session.query(WorkflowModel)\ + .filter_by(study_id=study_id)\ + .filter(WorkflowModel.workflow_spec_id != status_spec.id)\ + .all() + else: + # Get study workflows + workflow_models = session.query(WorkflowModel)\ + .filter_by(study_id=study_id)\ + .all() api_models = [] for workflow_model in workflow_models: processor = WorkflowProcessor(workflow_model, workflow_model.bpmn_workflow_json) - api_models.append(__get_workflow_api_model(processor)) + api_models.append(__get_workflow_api_model(processor, status_data)) schema = WorkflowApiSchema(many=True) return schema.dump(api_models) @@ -193,3 +254,4 @@ def map_pb_study_to_study(pb_study): study_info['inactive'] = False return study_info + diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 49990bec..25536423 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -17,7 +17,8 @@ def all_specifications(): @auth.login_required def add_workflow_specification(body): - new_spec = WorkflowSpecModelSchema().load(body, session=session) + new_spec: WorkflowSpecModel = WorkflowSpecModelSchema().load(body, session=session) + new_spec.is_status = new_spec.id == 'status' session.add(new_spec) session.commit() return WorkflowSpecModelSchema().dump(new_spec) @@ -69,9 +70,14 @@ def delete_workflow_specification(spec_id): session.commit() -def __get_workflow_api_model(processor: WorkflowProcessor): +def __get_workflow_api_model(processor: WorkflowProcessor, status_data=None): spiff_tasks = processor.get_all_user_tasks() user_tasks = list(map(Task.from_spiff, spiff_tasks)) + is_active = True + + if status_data is not None and processor.workflow_spec_id in status_data: + is_active = status_data[processor.workflow_spec_id] + workflow_api = WorkflowApi( id=processor.get_workflow_id(), status=processor.get_status(), @@ -80,7 +86,8 @@ def __get_workflow_api_model(processor: WorkflowProcessor): user_tasks=user_tasks, workflow_spec_id=processor.workflow_spec_id, spec_version=processor.get_spec_version(), - is_latest_spec=processor.get_spec_version() == processor.get_latest_version_string(processor.workflow_spec_id) + is_latest_spec=processor.get_spec_version() == processor.get_latest_version_string(processor.workflow_spec_id), + is_active=is_active ) if processor.next_task(): workflow_api.next_task = Task.from_spiff(processor.next_task()) @@ -89,9 +96,8 @@ def __get_workflow_api_model(processor: WorkflowProcessor): @auth.login_required def get_workflow(workflow_id, soft_reset=False, hard_reset=False): - workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() + workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first() processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset) - workflow_api_model = __get_workflow_api_model(processor) update_workflow_stats(workflow_model, workflow_api_model) return WorkflowApiSchema().dump(workflow_api_model) diff --git a/example_data.py b/example_data.py index 6659ac72..dc1d1869 100644 --- a/example_data.py +++ b/example_data.py @@ -82,11 +82,11 @@ class ExampleDataLoader: returns an array of data models to be added to the database.""" global file file_service = FileService() - spec = WorkflowSpecModel(id=id, name=name, display_name=display_name, - description=description) + description=description, + is_status=id == 'status') db.session.add(spec) db.session.commit() if not filepath: @@ -96,7 +96,7 @@ class ExampleDataLoader: noise, file_extension = os.path.splitext(file_path) filename = os.path.basename(file_path) - is_status = filename.lower() == 'status' + is_status = filename.lower() == 'status.bpmn' is_primary = filename.lower() == id + '.bpmn' try: file = open(file_path, 'rb') diff --git a/tests/data/status/pb_responses.dmn b/tests/data/status/crc2_training_session_data_security_plan.dmn similarity index 64% rename from tests/data/status/pb_responses.dmn rename to tests/data/status/crc2_training_session_data_security_plan.dmn index c91ec047..4acadb96 100644 --- a/tests/data/status/pb_responses.dmn +++ b/tests/data/status/crc2_training_session_data_security_plan.dmn @@ -1,6 +1,6 @@ - + @@ -8,11 +8,19 @@ - + + + + + + false + + + false @@ -21,6 +29,9 @@ true + + + true diff --git a/tests/data/status/random_fact.dmn b/tests/data/status/crc2_training_session_enter_core_info.dmn similarity index 81% rename from tests/data/status/random_fact.dmn rename to tests/data/status/crc2_training_session_enter_core_info.dmn index 8768deb9..2fbdd5ce 100644 --- a/tests/data/status/random_fact.dmn +++ b/tests/data/status/crc2_training_session_enter_core_info.dmn @@ -1,6 +1,6 @@ - + @@ -10,7 +10,7 @@ - + false diff --git a/tests/data/status/study_details.dmn b/tests/data/status/crc2_training_session_sponsor_funding_source.dmn similarity index 79% rename from tests/data/status/study_details.dmn rename to tests/data/status/crc2_training_session_sponsor_funding_source.dmn index dfca8117..e5020439 100644 --- a/tests/data/status/study_details.dmn +++ b/tests/data/status/crc2_training_session_sponsor_funding_source.dmn @@ -1,6 +1,6 @@ - + @@ -8,7 +8,7 @@ - + false diff --git a/tests/data/status/status.bpmn b/tests/data/status/status.bpmn index dd749bf7..13f144ed 100644 --- a/tests/data/status/status.bpmn +++ b/tests/data/status/status.bpmn @@ -5,41 +5,33 @@ SequenceFlow_1ees8ka - + Flow_1nimppb Flow_1txrak2 - + Flow_1m8285h Flow_1sggkit - + Flow_18pl92p Flow_0x9580l - - Flow_03u23vt - Flow_0pkxa8l - Flow_024q2cw Flow_1m8285h Flow_1nimppb - Flow_03u23vt Flow_18pl92p - Flow_1txrak2 - Flow_0pkxa8l Flow_1sggkit Flow_0x9580l Flow_0pwtiqm - Flow_0pwtiqm @@ -61,83 +53,68 @@ - + - - + + - + - + - - - - + - + - - - + + + - - - + + - - - - - - - - + + + - + - - - - - - + - - + + - - - + + + - - - + + - - - + + + - - + + - + diff --git a/tests/data/status/two_forms.dmn b/tests/data/status/two_forms.dmn deleted file mode 100644 index ca68ac3c..00000000 --- a/tests/data/status/two_forms.dmn +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - false - - - false - - - - - true - - - true - - - - - diff --git a/tests/test_study_api.py b/tests/test_study_api.py index 0b49f100..d41bb745 100644 --- a/tests/test_study_api.py +++ b/tests/test_study_api.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from unittest.mock import patch, Mock from crc import session -from crc.models.api_models import WorkflowApiSchema +from crc.models.api_models import WorkflowApiSchema, WorkflowApi from crc.models.study import StudyModel, StudyModelSchema from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudyDetailsSchema, \ ProtocolBuilderStudySchema @@ -160,9 +160,6 @@ class TestStudyApi(BaseTest): rv = self.app.delete('/v1.0/study/%i' % study.id) self.assert_failure(rv, error_code="study_integrity_error") - - - def test_delete_workflow(self): self.load_example_data() study = session.query(StudyModel).first() @@ -207,3 +204,56 @@ class TestStudyApi(BaseTest): json_data_after = json.loads(response_after.get_data(as_text=True)) workflows_after = WorkflowApiSchema(many=True).load(json_data_after) self.assertEqual(1, len(workflows_after)) + + """ + Workflow Specs that have been made available (or not) to a particular study via the status.bpmn should be flagged + as available (or not) when the list of a study's workflows is retrieved. + """ + def test_workflow_spec_status(self): + self.load_example_data() + study = session.query(StudyModel).first() + + # Add status workflow + self.load_test_spec('status') + + # Add status workflow to the study + status_spec = session.query(WorkflowSpecModel).filter_by(is_status=True).first() + add_status_response = self.app.post('/v1.0/study/%i/workflows' % study.id, + content_type="application/json", + headers=self.logged_in_headers(), + data=json.dumps(WorkflowSpecModelSchema().dump(status_spec))) + self.assert_success(add_status_response) + json_data_status = json.loads(add_status_response.get_data(as_text=True)) + status_workflow: WorkflowApi = WorkflowApiSchema().load(json_data_status) + status_task_id = status_workflow.next_task['id'] + + # Add all available non-status workflows to the study + specs = session.query(WorkflowSpecModel).filter_by(is_status=False).all() + for spec in specs: + add_response = self.app.post('/v1.0/study/%i/workflows' % study.id, + content_type="application/json", + headers=self.logged_in_headers(), + data=json.dumps(WorkflowSpecModelSchema().dump(spec))) + self.assert_success(add_response) + + for is_active in [False, True]: + # Update status task data + update_status_response = self.app.put('/v1.0/workflow/%i/task/%s/data' % (status_workflow.id, status_task_id), + headers=self.logged_in_headers(), + content_type="application/json", + data=json.dumps({'some_input': is_active})) + self.assert_success(update_status_response) + + # List workflows for study + response_after = self.app.get('/v1.0/study/%i/workflows' % study.id, + content_type="application/json", + headers=self.logged_in_headers()) + self.assert_success(response_after) + + json_data_after = json.loads(response_after.get_data(as_text=True)) + workflows_after = WorkflowApiSchema(many=True).load(json_data_after) + self.assertEqual(len(specs), len(workflows_after)) + + for workflow in workflows_after: + self.assertEqual(workflow.is_active, is_active) + diff --git a/tests/test_workflow_processor.py b/tests/test_workflow_processor.py index fed34567..1bba6d49 100644 --- a/tests/test_workflow_processor.py +++ b/tests/test_workflow_processor.py @@ -326,6 +326,9 @@ class TestWorkflowProcessor(BaseTest): def test_status_bpmn(self): self.load_example_data() + + specs = session.query(WorkflowSpecModel).all() + study = session.query(StudyModel).first() workflow_spec_model = self.load_test_spec("status") @@ -345,6 +348,5 @@ class TestWorkflowProcessor(BaseTest): self.assertEqual(processor.get_status(), WorkflowStatus.complete) # Enabled status of all specs should match the value set in the first task - for spec_id in ['two_forms', 'random_fact', 'pb_responses', 'study_details']: - self.assertEqual(task.data[spec_id], enabled) - + for spec in specs: + self.assertEqual(task.data[spec.id], enabled)