diff --git a/Pipfile b/Pipfile index 69d4dc7b..2be1123c 100644 --- a/Pipfile +++ b/Pipfile @@ -13,7 +13,7 @@ alembic = "*" coverage = "*" docxtpl = "*" flask = "*" -celery = "<5" +celery = "<6" flask-admin = "*" flask-bcrypt = "*" flask-cors = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 438497b9..39650371 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eb924ba10433552dcde585064a905bb8b78e96b28fc23391eee486ca34e22aad" + "sha256": "89204202883b76908f866ebe652cb214c079575f87cb05f677d602e3f9040c4b" }, "pipfile-spec": 6, "requires": { @@ -33,11 +33,11 @@ }, "amqp": { "hashes": [ - "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", - "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59" + "sha256:1e5f707424e544078ca196e72ae6a14887ce74e02bd126be54b7c03c971bef18", + "sha256:9cd81f7b023fc04bbb108718fbac674f06901b77bfcdce85b10e2a5d0ee91be5" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.6.1" + "markers": "python_version >= '3.6'", + "version": "==5.0.9" }, "aniso8601": { "hashes": [ @@ -89,7 +89,7 @@ "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" ], - "markers": "python_version >= '3.6' and python_version < '3.9'", + "markers": "python_version < '3.9'", "version": "==0.2.1" }, "bcrypt": { @@ -131,11 +131,11 @@ }, "celery": { "hashes": [ - "sha256:a92e1d56e650781fb747032a3997d16236d037c8199eacd5217d1a72893bca45", - "sha256:d220b13a8ed57c78149acf82c006785356071844afe0b27012a4991d44026f9f" + "sha256:2844eb040e915398623a43253a8e1016723442ece6b0751a3c416d8a2b34216f", + "sha256:5a68a351076cfac4f678fa5ffd898105c28825a2224902da006970005196d061" ], "index": "pypi", - "version": "==4.4.7" + "version": "==5.2.2" }, "certifi": { "hashes": [ @@ -215,6 +215,28 @@ "markers": "python_version >= '3.6'", "version": "==8.0.4" }, + "click-didyoumean": { + "hashes": [ + "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", + "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" + ], + "markers": "python_version < '4' and python_full_version >= '3.6.2'", + "version": "==0.3.0" + }, + "click-plugins": { + "hashes": [ + "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", + "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8" + ], + "version": "==1.1.1" + }, + "click-repl": { + "hashes": [ + "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b", + "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8" + ], + "version": "==0.2.0" + }, "clickclick": { "hashes": [ "sha256:4efb13e62353e34c5eef7ed6582c4920b418d7dedc86d819e22ee089ba01802c", @@ -530,7 +552,7 @@ "sha256:175f4ee440a0317f6e8d81b7f8d4869f93316170a65ad2b007d2929186c8052c", "sha256:e0bc84ff355328a4adfc5240c4f211e0ab386f80aa640d1b11f0618a1d282094" ], - "markers": "python_version < '3.10'", + "markers": "python_version < '3.9'", "version": "==4.11.1" }, "importlib-resources": { @@ -575,11 +597,11 @@ }, "kombu": { "hashes": [ - "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", - "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74" + "sha256:81a90c1de97e08d3db37dbf163eaaf667445e1068c98bfd89f051a40e9f6dbbd", + "sha256:eeaeb8024f3a5cfc71c9250e45cddb8493f269d74ada2f74909a93c59c4b4179" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.6.11" + "markers": "python_version >= '3.7'", + "version": "==5.2.3" }, "ldap3": { "hashes": [ @@ -813,6 +835,14 @@ "index": "pypi", "version": "==1.4.1" }, + "prompt-toolkit": { + "hashes": [ + "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c", + "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650" + ], + "markers": "python_full_version >= '3.6.2'", + "version": "==3.0.28" + }, "psycopg2-binary": { "hashes": [ "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7", @@ -1146,6 +1176,14 @@ "index": "pypi", "version": "==0.14.4" }, + "setuptools": { + "hashes": [ + "sha256:2347b2b432c891a863acadca2da9ac101eae6169b1d3dfee2ec605ecd50dbfe5", + "sha256:e4f30b9f84e5ab3decf945113119649fec09c1fc3507c6ebffec75646c56e62b" + ], + "markers": "python_version >= '3.7'", + "version": "==60.9.3" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -1313,11 +1351,11 @@ }, "vine": { "hashes": [ - "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", - "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" + "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", + "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.0" + "markers": "python_version >= '3.6'", + "version": "==5.0.0" }, "waitress": { "hashes": [ @@ -1327,6 +1365,13 @@ "markers": "python_version >= '3.6'", "version": "==2.0.0" }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + }, "webob": { "hashes": [ "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b", @@ -1437,7 +1482,7 @@ "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==3.7.0" } }, diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 1cf4f097..41d84394 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -244,9 +244,10 @@ def get_task_events(action = None, workflow = None, study = None): study = session.query(StudyModel).filter(StudyModel.id == event.study_id).first() workflow = session.query(WorkflowModel).filter(WorkflowModel.id == event.workflow_id).first() spec = WorkflowSpecService().get_spec(workflow.workflow_spec_id) - workflow_meta = WorkflowMetadata.from_workflow(workflow, spec) - if study and study.status in [StudyStatus.open_for_enrollment, StudyStatus.in_progress]: - task_events.append(TaskEvent(event, study, workflow_meta)) + if spec is not None: + workflow_meta = WorkflowMetadata.from_workflow(workflow, spec) + if study and study.status in [StudyStatus.open_for_enrollment, StudyStatus.in_progress]: + task_events.append(TaskEvent(event, study, workflow_meta)) return TaskEventSchema(many=True).dump(task_events) diff --git a/crc/scripts/delete_variables.py b/crc/scripts/delete_variables.py new file mode 100644 index 00000000..fe0af0fb --- /dev/null +++ b/crc/scripts/delete_variables.py @@ -0,0 +1,17 @@ +from crc.api.common import ApiError +from crc.scripts.script import Script + + +class DeleteVariables(Script): + + def get_description(self): + return """Script to delete variables from task_data, if they exist. + Accepts a list of variables to delete.""" + + 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): + for arg in args: + if arg in task.data: + del(task.data[arg]) diff --git a/crc/scripts/get_spec_from_id.py b/crc/scripts/get_spec_from_id.py index 915d709d..c94a5b81 100644 --- a/crc/scripts/get_spec_from_id.py +++ b/crc/scripts/get_spec_from_id.py @@ -4,7 +4,7 @@ from crc.scripts.script import Script from crc.services.workflow_spec_service import WorkflowSpecService -class ScriptTemplate(Script): +class GetSpecFromID(Script): def get_description(self): return """Get workflow spec information from a workflow spec id""" diff --git a/crc/scripts/get_spec_from_workflow_id.py b/crc/scripts/get_spec_from_workflow_id.py index 638e9288..53f7b99a 100644 --- a/crc/scripts/get_spec_from_workflow_id.py +++ b/crc/scripts/get_spec_from_workflow_id.py @@ -5,7 +5,7 @@ from crc.scripts.script import Script from crc.services.workflow_spec_service import WorkflowSpecService -class ScriptTemplate(Script): +class GetSpecFromWorkflowID(Script): def get_description(self): return """Get a workflow spec, from a workflow id. You must pass in a workflow id.""" diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index da809ae2..d899346e 100755 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -172,10 +172,9 @@ class WorkflowService(object): """Raise an exception of the workflow is not enabled and can not be executed.""" if study_id is not None: study_model = session.query(StudyModel).filter(StudyModel.id == study_id).first() - spec_model = session.query(WorkflowSpecModel).filter(WorkflowSpecModel.id == spec_id).first() status = StudyService._get_study_status(study_model) - if spec_model.id in status and status[spec_model.id]['status'] == 'disabled': - raise ApiError(code='disabled_workflow', message=f"This workflow is disabled. {status[spec_model.id]['message']}") + if spec_id in status and status[spec_id]['status'] == 'disabled': + raise ApiError(code='disabled_workflow', message=f"This workflow is disabled. {status[spec_id]['message']}") @staticmethod def test_spec(spec_id, validate_study_id=None, test_until=None, required_only=False): @@ -190,6 +189,7 @@ class WorkflowService(object): """ workflow_model = WorkflowService.make_test_workflow(spec_id, validate_study_id) + WorkflowService.raise_if_disabled(spec_id, validate_study_id) try: processor = WorkflowProcessor(workflow_model, validate_only=True) count = 0 diff --git a/tests/data/delete_variables/delete_variables.bpmn b/tests/data/delete_variables/delete_variables.bpmn new file mode 100644 index 00000000..cad85664 --- /dev/null +++ b/tests/data/delete_variables/delete_variables.bpmn @@ -0,0 +1,84 @@ + + + + + Flow_0zdl9n2 + + + + + + Flow_0aftk6v + + + + Flow_0zdl9n2 + Flow_14niiph + a_item = 1 +b_item = 2 +c_item = 'a string' +d_item = a_item + b_item +e_item = datetime.datetime.now() + + + Flow_0ke44z0 + Flow_1wjlzq0 + delete_variables('a_item', 'b_item', 'c_item', 'd_item', 'e_item') + + + ## Task Data +{{ task_data }} + Flow_1wjlzq0 + Flow_0aftk6v + + + + ## Task Data +{{ task_data }} + Flow_14niiph + Flow_0ke44z0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_workflow_api.py b/tests/test_workflow_api.py index db083390..ff8a93c2 100644 --- a/tests/test_workflow_api.py +++ b/tests/test_workflow_api.py @@ -1,11 +1,10 @@ from tests.base_test import BaseTest from crc import session +from crc.models.task_event import TaskEventModel from crc.models.user import UserModel -from crc.services.user_service import UserService from crc.services.workflow_service import WorkflowService - -from example_data import ExampleDataLoader +from crc.services.workflow_spec_service import WorkflowSpecService import json @@ -26,6 +25,60 @@ class TestWorkflowApi(BaseTest): headers=self.logged_in_headers()) self.assert_success(rv) + def test_get_task_events_bad_spec(self): + self.add_studies() + workflow = self.create_workflow('hello_world') + + # add a task_event + task_event = TaskEventModel( + study_id=workflow.study_id, + user_uid='dhf8r', + workflow_id=workflow.id, + workflow_spec_id=workflow.workflow_spec_id, + spec_version='', + action='', + task_id='', + task_name='my task name', + task_title='my task title', + task_state='', + task_lane='', + form_data='', + mi_type='', + mi_count=None, + mi_index=None, + process_name='', + date=None + ) + session.add(task_event) + session.commit() + + # make sure we have a task_event + tasks = session.query(TaskEventModel).filter(TaskEventModel.user_uid=='dhf8r').all() + self.assertEqual(1, len(tasks)) + + rv = self.app.get(f'/v1.0/task_events', + follow_redirects=True, + content_type="application/json", + headers=self.logged_in_headers()) + self.assert_success(rv) + data = json.loads(rv.get_data(as_text=True)) + # make sure we get the task_event + self.assertEqual('my task title', data[0]['task_title']) + + # delete the workflow spec + WorkflowSpecService().delete_spec('hello_world') + + # try to get the task_event again + rv = self.app.get(f'/v1.0/task_events', + follow_redirects=True, + content_type="application/json", + headers=self.logged_in_headers()) + # make sure we don't get an error, even though the spec no longer exists + self.assert_success(rv) + + # make sure we don't get any events back. + self.assertEqual([], rv.json) + def test_library_code(self): spec1 = self.load_test_spec('hello_world') diff --git a/tests/workflow/test_workflow_delete_variables.py b/tests/workflow/test_workflow_delete_variables.py new file mode 100644 index 00000000..578a3e34 --- /dev/null +++ b/tests/workflow/test_workflow_delete_variables.py @@ -0,0 +1,19 @@ +from tests.base_test import BaseTest + + +class TestDeleteVariables(BaseTest): + + def test_delete_variables(self): + """This workflow creates variables 'a', 'b', 'c', 'd', and 'e', + then deletes them with the delete_variables script. + We assert that they are no longer in task.data""" + workflow = self.create_workflow('delete_variables') + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + items = ('a_item', 'b_item', 'c_item', 'd_item', 'e_item') + for item in items: + self.assertIn(item, task.data) + workflow_api = self.complete_form(workflow, task, {}) + task = workflow_api.next_task + for item in items: + self.assertNotIn(item, task.data)