From d3462d2e1582ea8d569c08eb6436f04616840142 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 10 Aug 2020 07:42:56 -0600 Subject: [PATCH 1/7] Deleting all dependencies for a workflow --- crc/services/study_service.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/crc/services/study_service.py b/crc/services/study_service.py index 2be8ce20..3befb382 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -9,13 +9,14 @@ from ldap3.core.exceptions import LDAPSocketOpenError from crc import db, session, app from crc.api.common import ApiError -from crc.models.file import FileModel, FileModelSchema, File +from crc.models.approval import ApprovalFile, ApprovalModel +from crc.models.file import FileDataModel, FileModel, FileModelSchema, File, LookupFileModel, LookupDataModel from crc.models.ldap import LdapSchema from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata from crc.models.task_event import TaskEventModel, TaskEvent from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \ - WorkflowStatus + WorkflowStatus, WorkflowSpecDependencyFile from crc.services.approval_service import ApprovalService from crc.services.file_service import FileService from crc.services.ldap_service import LdapService @@ -83,13 +84,29 @@ class StudyService(object): session.commit() @staticmethod - def delete_workflow(workflow): - for file in session.query(FileModel).filter_by(workflow_id=workflow.id).all(): + def delete_workflow(workflow_id): + workflow = session.query(WorkflowModel).get(workflow_id) + for file in session.query(FileModel).filter_by(workflow_id=workflow_id).all(): FileService.delete_file(file.id) for dep in workflow.dependencies: session.delete(dep) session.query(TaskEventModel).filter_by(workflow_id=workflow.id).delete() - session.query(WorkflowModel).filter_by(id=workflow.id).delete() + session.query(ApprovalFile).filter(ApprovalModel.workflow_id==workflow_id).delete(synchronize_session='fetch') + session.query(ApprovalModel).filter_by(workflow_id=workflow.id).delete() + session.query(LookupDataModel).filter( + LookupFileModel.workflow_spec_id==workflow.workflow_spec_id + ).delete(synchronize_session='fetch') + session.query(LookupFileModel).filter_by(workflow_spec_id=workflow.workflow_spec_id).delete() + + session.query(WorkflowSpecDependencyFile).filter( + FileDataModel.file_model_id==FileModel.id, + FileModel.workflow_id==workflow_id + ).delete(synchronize_session='fetch') + + + session.query(FileDataModel).filter(FileModel.workflow_id==workflow_id).delete(synchronize_session='fetch') + session.query(FileModel).filter_by(workflow_id=workflow_id).delete() + session.delete(workflow) session.commit() @staticmethod From 56b161a234e705f29024b68eabb252dcad3a47d3 Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 10 Aug 2020 13:51:05 -0400 Subject: [PATCH 2/7] Fixing the failing tests by passing in workflow.id rather than workflow to the delete workflow endpoint --- crc/api/workflow.py | 2 +- crc/services/study_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 10fb3ce0..223f280f 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -91,7 +91,7 @@ def delete_workflow_specification(spec_id): # Delete all events and workflow models related to this specification for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id): - StudyService.delete_workflow(workflow) + StudyService.delete_workflow(workflow.id) session.query(WorkflowSpecModel).filter_by(id=spec_id).delete() session.commit() diff --git a/crc/services/study_service.py b/crc/services/study_service.py index 3befb382..fab76542 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -79,7 +79,7 @@ class StudyService(object): def delete_study(study_id): session.query(TaskEventModel).filter_by(study_id=study_id).delete() for workflow in session.query(WorkflowModel).filter_by(study_id=study_id): - StudyService.delete_workflow(workflow) + StudyService.delete_workflow(workflow.id) session.query(StudyModel).filter_by(id=study_id).delete() session.commit() From b53d0334002a4dbf1e2ccb26d3071f8ca6a08651 Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 10 Aug 2020 14:56:33 -0400 Subject: [PATCH 3/7] Cleaning up the delete workflow method in study service so we don't try to delete the same thing more than once) --- crc/services/study_service.py | 38 ++++++++++++++--------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/crc/services/study_service.py b/crc/services/study_service.py index fab76542..2ab774a5 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -60,7 +60,7 @@ class StudyService(object): study.approvals = ApprovalService.get_approvals_for_study(study.id) files = FileService.get_files_for_study(study.id) files = (File.from_models(model, FileService.get_file_data(model.id), - FileService.get_doc_dictionary()) for model in files) + FileService.get_doc_dictionary()) for model in files) study.files = list(files) # Calling this line repeatedly is very very slow. It creates the # master spec and runs it. Don't execute this for Abandoned studies, as @@ -86,26 +86,18 @@ class StudyService(object): @staticmethod def delete_workflow(workflow_id): workflow = session.query(WorkflowModel).get(workflow_id) - for file in session.query(FileModel).filter_by(workflow_id=workflow_id).all(): - FileService.delete_file(file.id) - for dep in workflow.dependencies: - session.delete(dep) + if not workflow: + return + session.query(TaskEventModel).filter_by(workflow_id=workflow.id).delete() - session.query(ApprovalFile).filter(ApprovalModel.workflow_id==workflow_id).delete(synchronize_session='fetch') - session.query(ApprovalModel).filter_by(workflow_id=workflow.id).delete() - session.query(LookupDataModel).filter( - LookupFileModel.workflow_spec_id==workflow.workflow_spec_id - ).delete(synchronize_session='fetch') - session.query(LookupFileModel).filter_by(workflow_spec_id=workflow.workflow_spec_id).delete() - - session.query(WorkflowSpecDependencyFile).filter( - FileDataModel.file_model_id==FileModel.id, - FileModel.workflow_id==workflow_id - ).delete(synchronize_session='fetch') - - - session.query(FileDataModel).filter(FileModel.workflow_id==workflow_id).delete(synchronize_session='fetch') + session.query(FileDataModel).filter(FileModel.workflow_id == workflow_id).delete(synchronize_session='fetch') session.query(FileModel).filter_by(workflow_id=workflow_id).delete() + # Workflow Dependencies should cascade delete, so no need to delete those seperately. + + # Todo: Remove approvals completely. + session.query(ApprovalFile).filter(ApprovalModel.workflow_id == workflow_id).delete(synchronize_session='fetch') + session.query(ApprovalModel).filter_by(workflow_id=workflow.id).delete() + session.delete(workflow) session.commit() @@ -244,9 +236,9 @@ class StudyService(object): @staticmethod def get_protocol(study_id): """Returns the study protocol, if it has been uploaded.""" - file = db.session.query(FileModel)\ - .filter_by(study_id=study_id)\ - .filter_by(form_field_key='Study_Protocol_Document')\ + file = db.session.query(FileModel) \ + .filter_by(study_id=study_id) \ + .filter_by(form_field_key='Study_Protocol_Document') \ .first() return FileModelSchema().dump(file) @@ -332,7 +324,7 @@ class StudyService(object): return WorkflowProcessor.run_master_spec(master_specs[0], study_model) @staticmethod - def _add_all_workflow_specs_to_study(study_model:StudyModel): + def _add_all_workflow_specs_to_study(study_model: StudyModel): existing_models = session.query(WorkflowModel).filter(WorkflowModel.study == study_model).all() existing_specs = list(m.workflow_spec_id for m in existing_models) new_specs = session.query(WorkflowSpecModel). \ From 3cfcd74a6aab5160f681576e59611dc4aa19ea90 Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 10 Aug 2020 15:16:53 -0400 Subject: [PATCH 4/7] Just finding a few more edge cases, all tests should pass now. --- crc/services/study_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crc/services/study_service.py b/crc/services/study_service.py index 2ab774a5..da480255 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -90,9 +90,9 @@ class StudyService(object): return session.query(TaskEventModel).filter_by(workflow_id=workflow.id).delete() + session.query(WorkflowSpecDependencyFile).filter_by(workflow_id=workflow_id).delete(synchronize_session='fetch') session.query(FileDataModel).filter(FileModel.workflow_id == workflow_id).delete(synchronize_session='fetch') - session.query(FileModel).filter_by(workflow_id=workflow_id).delete() - # Workflow Dependencies should cascade delete, so no need to delete those seperately. + session.query(FileModel).filter_by(workflow_id=workflow_id).delete(synchronize_session='fetch') # Todo: Remove approvals completely. session.query(ApprovalFile).filter(ApprovalModel.workflow_id == workflow_id).delete(synchronize_session='fetch') From 79c14ad23cf56462ef7c41169467520923ad60a0 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 10 Aug 2020 22:23:20 -0600 Subject: [PATCH 5/7] Unlinking file from workflow instead of trying to delete it --- crc/services/study_service.py | 3 +-- tests/study/test_study_api.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/crc/services/study_service.py b/crc/services/study_service.py index da480255..e183cd15 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -91,8 +91,7 @@ class StudyService(object): session.query(TaskEventModel).filter_by(workflow_id=workflow.id).delete() session.query(WorkflowSpecDependencyFile).filter_by(workflow_id=workflow_id).delete(synchronize_session='fetch') - session.query(FileDataModel).filter(FileModel.workflow_id == workflow_id).delete(synchronize_session='fetch') - session.query(FileModel).filter_by(workflow_id=workflow_id).delete(synchronize_session='fetch') + session.query(FileModel).filter_by(workflow_id=workflow_id).update({'archived': True, 'workflow_id': None}) # Todo: Remove approvals completely. session.query(ApprovalFile).filter(ApprovalModel.workflow_id == workflow_id).delete(synchronize_session='fetch') diff --git a/tests/study/test_study_api.py b/tests/study/test_study_api.py index 6bb601ce..1913f5ff 100644 --- a/tests/study/test_study_api.py +++ b/tests/study/test_study_api.py @@ -10,6 +10,7 @@ from crc import session, app from crc.models.protocol_builder import ProtocolBuilderStatus, \ ProtocolBuilderStudySchema from crc.models.approval import ApprovalStatus +from crc.models.file import FileModel from crc.models.task_event import TaskEventModel from crc.models.study import StudyModel, StudySchema, StudyStatus from crc.models.workflow import WorkflowSpecModel, WorkflowModel @@ -235,13 +236,36 @@ class TestStudyApi(BaseTest): self.assertEqual(study.sponsor, json_data['sponsor']) self.assertEqual(study.ind_number, json_data['ind_number']) - def test_delete_study(self): self.load_example_data() study = session.query(StudyModel).first() rv = self.app.delete('/v1.0/study/%i' % study.id, headers=self.logged_in_headers()) self.assert_success(rv) + def test_delete_workflow(self): + self.load_example_data() + workflow = session.query(WorkflowModel).first() + FileService.add_workflow_file(workflow_id=workflow.id, + name="anything.png", content_type="text", + binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr" ) + + workflow_files = session.query(FileModel).filter_by(workflow_id=workflow.id) + self.assertEqual(workflow_files.count(), 1) + workflow_files_ids = [file.id for file in workflow_files] + + rv = self.app.delete(f'/v1.0/workflow/{workflow.id}', headers=self.logged_in_headers()) + self.assert_success(rv) + + # No files should have the deleted workflow id anymore + workflow_files = session.query(FileModel).filter_by(workflow_id=workflow.id) + self.assertEqual(workflow_files.count(), 0) + + # Finally, let's confirm the file was archived + workflow_files = session.query(FileModel).filter(FileModel.id.in_(workflow_files_ids)) + for file in workflow_files: + self.assertTrue(file.archived) + self.assertIsNone(file.workflow_id) + def test_delete_study_with_workflow_and_status(self): self.load_example_data() workflow = session.query(WorkflowModel).first() From 85ad477b2bd8ee41fd66e98bb7af924449dc918d Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 17 Aug 2020 14:56:00 -0400 Subject: [PATCH 6/7] Some minor cleanup on the study status and automatic events. I wanted to avoid having one database model automatically generating other database models as a side effect. The study service now has full responsiblity to recording study events. To help in running tests, adding __init__.py methods to all the test directories. --- crc/api/study.py | 28 +++++++++++++------ crc/models/study.py | 48 --------------------------------- crc/services/study_service.py | 51 ++++++++++++++++++++++++++--------- tests/approval/__init__.py | 0 tests/files/__init__.py | 0 tests/ldap/__init__.py | 0 tests/study/__init__.py | 0 tests/study/test_study_api.py | 8 +++--- tests/workflow/__init__.py | 0 9 files changed, 62 insertions(+), 73 deletions(-) create mode 100644 tests/approval/__init__.py create mode 100644 tests/files/__init__.py create mode 100644 tests/ldap/__init__.py create mode 100644 tests/study/__init__.py create mode 100644 tests/workflow/__init__.py diff --git a/crc/api/study.py b/crc/api/study.py index 5d641105..e6e1ed20 100644 --- a/crc/api/study.py +++ b/crc/api/study.py @@ -23,14 +23,12 @@ def add_study(body): primary_investigator_id=body['primary_investigator_id'], last_updated=datetime.now(), status=StudyStatus.in_progress) - - study_model.update_event( - status=StudyStatus.in_progress, - event_type=StudyEventType.automatic, - user_uid=g.user.uid - ) - session.add(study_model) + StudyService.add_study_update_event(study_model, + status=StudyStatus.in_progress, + event_type=StudyEventType.user, + user_uid=g.user.uid) + errors = StudyService._add_all_workflow_specs_to_study(study_model) session.commit() study = StudyService().get_study(study_model.id) @@ -40,6 +38,7 @@ def add_study(body): def update_study(study_id, body): + """Pretty limited, but allows manual modifications to the study status """ if study_id is None: raise ApiError('unknown_study', 'Please provide a valid Study ID.') @@ -48,7 +47,20 @@ def update_study(study_id, body): raise ApiError('unknown_study', 'The study "' + study_id + '" is not recognized.') study: Study = StudyForUpdateSchema().load(body) - study.update_model(study_model) + + status = StudyStatus(study.status) + study_model.last_updated = datetime.now() + + if study_model.status != status: + study_model.status = status + StudyService.add_study_update_event(study_model, status, StudyEventType.user, + user_uid=UserService.current_user().uid if UserService.has_user() else None, + comment='' if not hasattr(study, 'comment') else study.comment, + ) + + if status == StudyStatus.open_for_enrollment: + study_model.enrollment_date = study.enrollment_date + session.add(study_model) session.commit() # Need to reload the full study to return it to the frontend diff --git a/crc/models/study.py b/crc/models/study.py index b43242f6..811ab73e 100644 --- a/crc/models/study.py +++ b/crc/models/study.py @@ -61,36 +61,6 @@ class StudyModel(db.Model): self.irb_status = IrbStatus.incomplete_in_protocol_builder self.status = StudyStatus.in_progress - self.update_event( - status=StudyStatus.in_progress, - event_type=StudyEventType.automatic, - user_uid=self.user_uid - ) - - if pbs.HSRNUMBER: - self.irb_status = IrbStatus.hsr_assigned - self.status = StudyStatus.open_for_enrollment - self.update_event( - status=StudyStatus.open_for_enrollment, - event_type=StudyEventType.automatic, - user_uid=self.user_uid - ) - if self.on_hold: - self.status = StudyStatus.hold - self.update_event( - status=StudyStatus.hold, - event_type=StudyEventType.automatic, - user_uid=self.user_uid - ) - - def update_event(self, status, event_type, user_uid, comment=''): - study_event = StudyEvent(study=self, - status=status, - event_type=event_type, - user_uid=user_uid, - comment=comment) - db.session.add(study_event) - db.session.commit() class StudyEvent(db.Model): @@ -202,24 +172,6 @@ class Study(object): instance = cls(**args) return instance - def update_model(self, study_model: StudyModel): - """As the case for update was very reduced, it's mostly and specifically - updating only the study status and generating a history record - """ - status = StudyStatus(self.status) - study_model.last_updated = datetime.datetime.now() - study_model.status = status - - if status == StudyStatus.open_for_enrollment: - study_model.enrollment_date = self.enrollment_date - - study_model.update_event( - status=status, - comment='' if not hasattr(self, 'comment') else self.comment, - event_type=StudyEventType.user, - user_uid=UserService.current_user().uid if UserService.has_user() else None - ) - def model_args(self): """Arguments that can be passed into the Study Model to update it.""" self_dict = self.__dict__.copy() diff --git a/crc/services/study_service.py b/crc/services/study_service.py index d1e00fbc..0ac1a3aa 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -12,7 +12,8 @@ from crc.api.common import ApiError from crc.models.file import FileModel, FileModelSchema, File from crc.models.ldap import LdapSchema from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus -from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata, StudyEventType +from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata, StudyEventType, StudyEvent, \ + IrbStatus from crc.models.task_event import TaskEventModel, TaskEvent from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \ WorkflowStatus @@ -60,7 +61,7 @@ class StudyService(object): study.approvals = ApprovalService.get_approvals_for_study(study.id) files = FileService.get_files_for_study(study.id) files = (File.from_models(model, FileService.get_file_data(model.id), - FileService.get_doc_dictionary()) for model in files) + FileService.get_doc_dictionary()) for model in files) study.files = list(files) # Calling this line repeatedly is very very slow. It creates the # master spec and runs it. Don't execute this for Abandoned studies, as @@ -229,9 +230,9 @@ class StudyService(object): @staticmethod def get_protocol(study_id): """Returns the study protocol, if it has been uploaded.""" - file = db.session.query(FileModel)\ - .filter_by(study_id=study_id)\ - .filter_by(form_field_key='Study_Protocol_Document')\ + file = db.session.query(FileModel) \ + .filter_by(study_id=study_id) \ + .filter_by(form_field_key='Study_Protocol_Document') \ .first() return FileModelSchema().dump(file) @@ -253,30 +254,54 @@ class StudyService(object): db_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all() # Update all studies from the protocol builder, create new studies as needed. - # Futher assures that every active study (that does exist in the protocol builder) + # Further assures that every active study (that does exist in the protocol builder) # has a reference to every available workflow (though some may not have started yet) for pb_study in pb_studies: + new_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 session.add(db_study) db_studies.append(db_study) + + if pb_study.HSRNUMBER: + db_study.irb_status = IrbStatus.hsr_assigned + if db_study.status != StudyStatus.open_for_enrollment: + new_status = StudyStatus.open_for_enrollment + db_study.update_from_protocol_builder(pb_study) StudyService._add_all_workflow_specs_to_study(db_study) + # 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 + StudyService.add_study_update_event(db_study, + status=new_status, + event_type=StudyEventType.automatic) + # Mark studies as inactive that are no longer in Protocol Builder for study in db_studies: pb_study = next((pbs for pbs in pb_studies if pbs.STUDYID == study.id), None) - if not pb_study: + if not pb_study and study.status != StudyStatus.abandoned: study.status = StudyStatus.abandoned - study.update_event( - status=StudyStatus.abandoned, - event_type=StudyEventType.automatic, - user_uid=study.user_uid - ) + StudyService.add_study_update_event(study, + status=StudyStatus.abandoned, + event_type=StudyEventType.automatic) db.session.commit() + @staticmethod + def add_study_update_event(study, status, event_type, user_uid=None, comment=''): + study_event = StudyEvent(study=study, + status=status, + event_type=event_type, + user_uid=user_uid, + comment=comment) + db.session.add(study_event) + db.session.commit() + @staticmethod def __update_status_of_workflow_meta(workflow_metas, status): # Update the status on each workflow @@ -322,7 +347,7 @@ class StudyService(object): return WorkflowProcessor.run_master_spec(master_specs[0], study_model) @staticmethod - def _add_all_workflow_specs_to_study(study_model:StudyModel): + def _add_all_workflow_specs_to_study(study_model: StudyModel): existing_models = session.query(WorkflowModel).filter(WorkflowModel.study == study_model).all() existing_specs = list(m.workflow_spec_id for m in existing_models) new_specs = session.query(WorkflowSpecModel). \ diff --git a/tests/approval/__init__.py b/tests/approval/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/files/__init__.py b/tests/files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ldap/__init__.py b/tests/ldap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/study/__init__.py b/tests/study/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/study/test_study_api.py b/tests/study/test_study_api.py index 42b230dd..717cf9cf 100644 --- a/tests/study/test_study_api.py +++ b/tests/study/test_study_api.py @@ -135,7 +135,7 @@ class TestStudyApi(BaseTest): study_event = session.query(StudyEvent).first() self.assertIsNotNone(study_event) self.assertEqual(study_event.status, StudyStatus.in_progress) - self.assertEqual(study_event.event_type, StudyEventType.automatic) + self.assertEqual(study_event.event_type, StudyEventType.user) self.assertFalse(study_event.comment) self.assertEqual(study_event.user_uid, self.test_uid) @@ -145,7 +145,7 @@ class TestStudyApi(BaseTest): study: StudyModel = session.query(StudyModel).first() study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility" study_schema = StudySchema().dump(study) - study_schema['status'] = StudyStatus.in_progress.value + study_schema['status'] = StudyStatus.hold.value study_schema['comment'] = update_comment rv = self.app.put('/v1.0/study/%i' % study.id, content_type="application/json", @@ -159,7 +159,7 @@ class TestStudyApi(BaseTest): # Making sure events history is being properly recorded study_event = session.query(StudyEvent).first() self.assertIsNotNone(study_event) - self.assertEqual(study_event.status, StudyStatus.in_progress) + self.assertEqual(study_event.status, StudyStatus.hold) self.assertEqual(study_event.event_type, StudyEventType.user) self.assertEqual(study_event.comment, update_comment) self.assertEqual(study_event.user_uid, self.test_uid) @@ -221,7 +221,7 @@ class TestStudyApi(BaseTest): # Automatic events check in_progress_events = session.query(StudyEvent).filter_by(status=StudyStatus.in_progress) - self.assertEqual(in_progress_events.count(), 3) # 3 studies were started + self.assertEqual(in_progress_events.count(), 1) # 1 study is in progress abandoned_events = session.query(StudyEvent).filter_by(status=StudyStatus.abandoned) self.assertEqual(abandoned_events.count(), 1) # 1 study has been abandoned diff --git a/tests/workflow/__init__.py b/tests/workflow/__init__.py new file mode 100644 index 00000000..e69de29b From dbd0dbf608fe1fbe1ca0d53a6c0977b9ec42d9dd Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 17 Aug 2020 14:56:24 -0400 Subject: [PATCH 7/7] mend --- tests/emails/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/emails/__init__.py diff --git a/tests/emails/__init__.py b/tests/emails/__init__.py new file mode 100644 index 00000000..e69de29b