diff --git a/crc/api.yml b/crc/api.yml index 980d6d13..daa1a0d3 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -425,7 +425,7 @@ paths: - name: spec_id in: path required: true - description: The unique id of an existing workflow specification to modify. + description: The unique id of an existing workflow specification. schema: type: string get: @@ -440,6 +440,18 @@ paths: application/json: schema: $ref: "#/components/schemas/WorkflowSpec" + post: + operationId: crc.api.workflow.get_workflow_from_spec + summary: Creates a workflow from a workflow spec and returns the workflow + tags: + - Workflow Specifications + responses: + '200': + description: Workflow generated successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Workflow" put: operationId: crc.api.workflow.update_workflow_specification security: @@ -469,6 +481,21 @@ paths: responses: '204': description: The workflow specification has been removed. + /workflow-specification/standalone: + get: + operationId: crc.api.workflow.standalone_workflow_specs + summary: Provides a list of workflow specifications that can be run outside a study. + tags: + - Workflow Specifications + responses: + '200': + description: A list of workflow specifications + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkflowSpec" /workflow-specification/{spec_id}/validate: parameters: - name: spec_id @@ -1536,6 +1563,9 @@ components: category_id: type: integer nullable: true + standalone: + type: boolean + example: false workflow_spec_category: $ref: "#/components/schemas/WorkflowSpecCategory" is_status: @@ -1608,6 +1638,8 @@ components: type: integer num_tasks_incomplete: type: integer + study_id: + type: integer example: id: 291234 diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 4bcfdabc..b51cdb2c 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -101,6 +101,24 @@ def delete_workflow_specification(spec_id): session.commit() +def get_workflow_from_spec(spec_id): + workflow_model = WorkflowService.get_workflow_from_spec(spec_id, g.user) + processor = WorkflowProcessor(workflow_model) + + processor.do_engine_steps() + processor.save() + WorkflowService.update_task_assignments(processor) + + workflow_api_model = WorkflowService.processor_to_workflow_api(processor) + return WorkflowApiSchema().dump(workflow_api_model) + + +def standalone_workflow_specs(): + schema = WorkflowSpecModelSchema(many=True) + specs = WorkflowService.get_standalone_workflow_specs() + return schema.dump(specs) + + def get_workflow(workflow_id, do_engine_steps=True): """Retrieve workflow based on workflow_id, and return it in the last saved State. If do_engine_steps is False, return the workflow without running any engine tasks or logging any events. """ @@ -185,9 +203,6 @@ def update_task(workflow_id, task_id, body, terminate_loop=None, update_all=Fals if workflow_model is None: raise ApiError("invalid_workflow_id", "The given workflow id is not valid.", status_code=404) - elif workflow_model.study is None: - raise ApiError("invalid_study", "There is no study associated with the given workflow.", status_code=404) - processor = WorkflowProcessor(workflow_model) task_id = uuid.UUID(task_id) spiff_task = processor.bpmn_workflow.get_task(task_id) diff --git a/crc/models/api_models.py b/crc/models/api_models.py index 2bb4abc6..308823fc 100644 --- a/crc/models/api_models.py +++ b/crc/models/api_models.py @@ -191,7 +191,7 @@ class DocumentDirectory(object): class WorkflowApi(object): def __init__(self, id, status, next_task, navigation, spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, - last_updated, is_review, title): + last_updated, is_review, title, study_id): self.id = id self.status = status self.next_task = next_task # The next task that requires user input. @@ -204,13 +204,14 @@ class WorkflowApi(object): self.last_updated = last_updated self.title = title self.is_review = is_review + self.study_id = study_id or '' class WorkflowApiSchema(ma.Schema): class Meta: model = WorkflowApi fields = ["id", "status", "next_task", "navigation", "workflow_spec_id", "spec_version", "is_latest_spec", "total_tasks", "completed_tasks", - "last_updated", "is_review", "title"] + "last_updated", "is_review", "title", "study_id"] unknown = INCLUDE status = EnumField(WorkflowStatus) @@ -221,7 +222,7 @@ class WorkflowApiSchema(ma.Schema): def make_workflow(self, data, **kwargs): keys = ['id', 'status', 'next_task', 'navigation', 'workflow_spec_id', 'spec_version', 'is_latest_spec', "total_tasks", "completed_tasks", - "last_updated", "is_review", "title"] + "last_updated", "is_review", "title", "study_id"] filtered_fields = {key: data[key] for key in keys} filtered_fields['next_task'] = TaskSchema().make_task(data['next_task']) return WorkflowApi(**filtered_fields) diff --git a/crc/models/task_event.py b/crc/models/task_event.py index 21a239ee..70bb1be5 100644 --- a/crc/models/task_event.py +++ b/crc/models/task_event.py @@ -10,7 +10,7 @@ from crc.services.ldap_service import LdapService class TaskEventModel(db.Model): __tablename__ = 'task_event' id = db.Column(db.Integer, primary_key=True) - study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=False) + study_id = db.Column(db.Integer, db.ForeignKey('study.id')) user_uid = db.Column(db.String, nullable=False) # In some cases the unique user id may not exist in the db yet. workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False) workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id')) diff --git a/crc/models/workflow.py b/crc/models/workflow.py index 0da32aec..10805a9a 100644 --- a/crc/models/workflow.py +++ b/crc/models/workflow.py @@ -33,6 +33,7 @@ class WorkflowSpecModel(db.Model): category_id = db.Column(db.Integer, db.ForeignKey('workflow_spec_category.id'), nullable=True) category = db.relationship("WorkflowSpecCategoryModel") is_master_spec = db.Column(db.Boolean, default=False) + standalone = db.Column(db.Boolean, default=False) class WorkflowSpecModelSchema(SQLAlchemyAutoSchema): @@ -88,6 +89,7 @@ class WorkflowModel(db.Model): total_tasks = db.Column(db.Integer, default=0) completed_tasks = db.Column(db.Integer, default=0) last_updated = db.Column(db.DateTime) + user_id = db.Column(db.String, default=None) # Order By is important or generating hashes on reviews. dependencies = db.relationship(WorkflowSpecDependencyFile, cascade="all, delete, delete-orphan", order_by="WorkflowSpecDependencyFile.file_data_id") diff --git a/crc/services/study_service.py b/crc/services/study_service.py index 749a4e14..9bc5a598 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -495,6 +495,7 @@ class StudyService(object): def _create_workflow_model(study: StudyModel, spec): workflow_model = WorkflowModel(status=WorkflowStatus.not_started, study=study, + user_id=None, workflow_spec_id=spec.id, last_updated=datetime.now()) session.add(workflow_model) diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index 265b61e7..2369ceaf 100644 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -408,7 +408,8 @@ class WorkflowService(object): completed_tasks=processor.workflow_model.completed_tasks, last_updated=processor.workflow_model.last_updated, is_review=is_review, - title=spec.display_name + title=spec.display_name, + study_id=processor.workflow_model.study_id or None ) if not next_task: # The Next Task can be requested to be a certain task, useful for parallel tasks. # This may or may not work, sometimes there is no next task to complete. @@ -667,30 +668,39 @@ class WorkflowService(object): @staticmethod def get_users_assigned_to_task(processor, spiff_task) -> List[str]: - if not hasattr(spiff_task.task_spec, 'lane') or spiff_task.task_spec.lane is None: - associated = StudyService.get_study_associates(processor.workflow_model.study.id) - return [user['uid'] for user in associated if user['access']] - if spiff_task.task_spec.lane not in spiff_task.data: - return [] # No users are assignable to the task at this moment - lane_users = spiff_task.data[spiff_task.task_spec.lane] - if not isinstance(lane_users, list): - lane_users = [lane_users] + if processor.workflow_model.study_id is None and processor.workflow_model.user_id is None: + raise ApiError.from_task(code='invalid_workflow', + message='A workflow must have either a study_id or a user_id.', + task=spiff_task) + # Standalone workflow - we only care about the current user + elif processor.workflow_model.study_id is None and processor.workflow_model.user_id is not None: + return [processor.workflow_model.user_id] + # Workflow associated with a study - get all the users + else: + if not hasattr(spiff_task.task_spec, 'lane') or spiff_task.task_spec.lane is None: + associated = StudyService.get_study_associates(processor.workflow_model.study.id) + return [user['uid'] for user in associated if user['access']] + if spiff_task.task_spec.lane not in spiff_task.data: + return [] # No users are assignable to the task at this moment + lane_users = spiff_task.data[spiff_task.task_spec.lane] + if not isinstance(lane_users, list): + lane_users = [lane_users] - lane_uids = [] - for user in lane_users: - if isinstance(user, dict): - if 'value' in user and user['value'] is not None: - lane_uids.append(user['value']) + lane_uids = [] + for user in lane_users: + if isinstance(user, dict): + if 'value' in user and user['value'] is not None: + lane_uids.append(user['value']) + else: + raise ApiError.from_task(code="task_lane_user_error", message="Spiff Task %s lane user dict must have a key called 'value' with the user's uid in it." % + spiff_task.task_spec.name, task=spiff_task) + elif isinstance(user, str): + lane_uids.append(user) else: - raise ApiError.from_task(code="task_lane_user_error", message="Spiff Task %s lane user dict must have a key called 'value' with the user's uid in it." % - spiff_task.task_spec.name, task=spiff_task) - elif isinstance(user, str): - lane_uids.append(user) - else: - raise ApiError.from_task(code="task_lane_user_error", message="Spiff Task %s lane user is not a string or dict" % - spiff_task.task_spec.name, task=spiff_task) + raise ApiError.from_task(code="task_lane_user_error", message="Spiff Task %s lane user is not a string or dict" % + spiff_task.task_spec.name, task=spiff_task) - return lane_uids + return lane_uids @staticmethod def log_task_action(user_uid, processor, spiff_task, action): @@ -783,3 +793,19 @@ class WorkflowService(object): for workflow in workflows: if workflow.status == WorkflowStatus.user_input_required or workflow.status == WorkflowStatus.waiting: WorkflowProcessor.reset(workflow, clear_data=False) + + @staticmethod + def get_workflow_from_spec(workflow_spec_id, user): + workflow_model = WorkflowModel(status=WorkflowStatus.not_started, + study=None, + user_id=user.uid, + workflow_spec_id=workflow_spec_id, + last_updated=datetime.now()) + db.session.add(workflow_model) + db.session.commit() + return workflow_model + + @staticmethod + def get_standalone_workflow_specs(): + specs = db.session.query(WorkflowSpecModel).filter_by(standalone=True).all() + return specs diff --git a/example_data.py b/example_data.py index f3918846..bc7c438c 100644 --- a/example_data.py +++ b/example_data.py @@ -266,7 +266,7 @@ class ExampleDataLoader: from_tests=True) def create_spec(self, id, name, display_name="", description="", filepath=None, master_spec=False, - category_id=None, display_order=None, from_tests=False): + category_id=None, display_order=None, from_tests=False, standalone=False): """Assumes that a directory exists in static/bpmn with the same name as the given id. further assumes that the [id].bpmn is the primary file for the workflow. returns an array of data models to be added to the database.""" @@ -278,7 +278,8 @@ class ExampleDataLoader: description=description, is_master_spec=master_spec, category_id=category_id, - display_order=display_order) + display_order=display_order, + standalone=standalone) db.session.add(spec) db.session.commit() if not filepath and not from_tests: diff --git a/migrations/versions/8b976945a54e_.py b/migrations/versions/8b976945a54e_.py new file mode 100644 index 00000000..0973f193 --- /dev/null +++ b/migrations/versions/8b976945a54e_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 8b976945a54e +Revises: c872232ebdcb +Create Date: 2021-04-18 11:42:41.894378 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8b976945a54e' +down_revision = 'c872232ebdcb' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('workflow', sa.Column('user_id', sa.String(), nullable=True)) + op.add_column('workflow_spec', sa.Column('standalone', sa.Boolean(), default=False)) + op.execute("UPDATE workflow_spec SET standalone=False WHERE standalone is null;") + op.execute("ALTER TABLE task_event ALTER COLUMN study_id DROP NOT NULL") + + +def downgrade(): + op.execute("UPDATE workflow SET user_id=NULL WHERE user_id is not NULL") + op.drop_column('workflow', 'user_id') + op.drop_column('workflow_spec', 'standalone') + op.execute("ALTER TABLE task_event ALTER COLUMN study_id SET NOT NULL ") diff --git a/tests/base_test.py b/tests/base_test.py index 4663ac61..8c64b548 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -175,11 +175,6 @@ class BaseTest(unittest.TestCase): specs = session.query(WorkflowSpecModel).all() self.assertIsNotNone(specs) - for spec in specs: - files = session.query(FileModel).filter_by(workflow_spec_id=spec.id).all() - self.assertIsNotNone(files) - self.assertGreater(len(files), 0) - for spec in specs: files = session.query(FileModel).filter_by(workflow_spec_id=spec.id).all() self.assertIsNotNone(files) @@ -379,6 +374,10 @@ class BaseTest(unittest.TestCase): def complete_form(self, workflow_in, task_in, dict_data, update_all=False, error_code=None, terminate_loop=None, user_uid="dhf8r"): + # workflow_in should be a workflow, not a workflow_api + # we were passing in workflow_api in many of our tests, and + # this caused problems testing standalone workflows + standalone = getattr(workflow_in.workflow_spec, 'standalone', False) prev_completed_task_count = workflow_in.completed_tasks if isinstance(task_in, dict): task_id = task_in["id"] @@ -421,7 +420,8 @@ class BaseTest(unittest.TestCase): .order_by(TaskEventModel.date.desc()).all() self.assertGreater(len(task_events), 0) event = task_events[0] - self.assertIsNotNone(event.study_id) + if not standalone: + self.assertIsNotNone(event.study_id) self.assertEqual(user_uid, event.user_uid) self.assertEqual(workflow.id, event.workflow_id) self.assertEqual(workflow.workflow_spec_id, event.workflow_spec_id) diff --git a/tests/data/hello_world/hello_world.bpmn b/tests/data/hello_world/hello_world.bpmn new file mode 100644 index 00000000..168aee1a --- /dev/null +++ b/tests/data/hello_world/hello_world.bpmn @@ -0,0 +1,58 @@ + + + + This workflow asks for a name and says hello + + SequenceFlow_0qyd2b7 + + + + Hello + + + + + + SequenceFlow_0qyd2b7 + SequenceFlow_1h46b40 + + + + Hello {{name}} + SequenceFlow_1h46b40 + SequenceFlow_0lqrc6e + + + SequenceFlow_0lqrc6e + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/study/test_study_cancellations.py b/tests/study/test_study_cancellations.py index 4b0d61f9..df0ba2a2 100644 --- a/tests/study/test_study_cancellations.py +++ b/tests/study/test_study_cancellations.py @@ -72,7 +72,7 @@ class TestStudyCancellations(BaseTest): workflow, study_id = self.load_workflow() workflow_api, first_task = self.get_first_task(workflow) - self.complete_form(workflow_api, first_task, {}) + self.complete_form(workflow, first_task, {}) study_result = self.put_study_on_hold(study_id) self.assertEqual('New Title', study_result.title) @@ -82,10 +82,10 @@ class TestStudyCancellations(BaseTest): workflow, study_id = self.load_workflow() workflow_api, first_task = self.get_first_task(workflow) - self.complete_form(workflow_api, first_task, {}) + self.complete_form(workflow, first_task, {}) workflow_api, next_task = self.get_second_task(workflow) - self.complete_form(workflow_api, next_task, {'how_many': 3}) + self.complete_form(workflow, next_task, {'how_many': 3}) study_result = self.put_study_on_hold(study_id) self.assertEqual('Second Title', study_result.title) @@ -95,13 +95,13 @@ class TestStudyCancellations(BaseTest): workflow, study_id = self.load_workflow() workflow_api, first_task = self.get_first_task(workflow) - self.complete_form(workflow_api, first_task, {}) + self.complete_form(workflow, first_task, {}) workflow_api, second_task = self.get_second_task(workflow) - self.complete_form(workflow_api, second_task, {'how_many': 3}) + self.complete_form(workflow, second_task, {'how_many': 3}) workflow_api, third_task = self.get_third_task(workflow) - self.complete_form(workflow_api, third_task, {}) + self.complete_form(workflow, third_task, {}) study_result = self.put_study_on_hold(study_id) self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title) diff --git a/tests/test_auto_set_primary_bpmn.py b/tests/test_auto_set_primary_bpmn.py index d6280b69..6179d2f4 100644 --- a/tests/test_auto_set_primary_bpmn.py +++ b/tests/test_auto_set_primary_bpmn.py @@ -13,7 +13,8 @@ class TestAutoSetPrimaryBPMN(BaseTest): category_id = session.query(WorkflowSpecCategoryModel).first().id # Add a workflow spec spec = WorkflowSpecModel(id='make_cookies', name='make_cookies', display_name='Cooooookies', - description='Om nom nom delicious cookies', category_id=category_id) + description='Om nom nom delicious cookies', category_id=category_id, + standalone=False) rv = self.app.post('/v1.0/workflow-specification', headers=self.logged_in_headers(), content_type="application/json", diff --git a/tests/test_email_script.py b/tests/test_email_script.py index 980d0c06..7786b045 100644 --- a/tests/test_email_script.py +++ b/tests/test_email_script.py @@ -23,7 +23,6 @@ class TestEmailScript(BaseTest): first_task = self.get_workflow_api(workflow).next_task - workflow = self.get_workflow_api(workflow) self.complete_form(workflow, first_task, {'subject': 'My Email Subject', 'recipients': 'test@example.com'}) self.assertEqual(1, len(outbox)) @@ -49,7 +48,6 @@ class TestEmailScript(BaseTest): def test_bad_email_address_1(self): workflow = self.create_workflow('email_script') first_task = self.get_workflow_api(workflow).next_task - workflow = self.get_workflow_api(workflow) with self.assertRaises(AssertionError): self.complete_form(workflow, first_task, {'recipients': 'test@example'}) @@ -57,7 +55,6 @@ class TestEmailScript(BaseTest): def test_bad_email_address_2(self): workflow = self.create_workflow('email_script') first_task = self.get_workflow_api(workflow).next_task - workflow = self.get_workflow_api(workflow) with self.assertRaises(AssertionError): self.complete_form(workflow, first_task, {'recipients': 'test'}) diff --git a/tests/test_launch_workflow_outside_study.py b/tests/test_launch_workflow_outside_study.py new file mode 100644 index 00000000..fbf51cc9 --- /dev/null +++ b/tests/test_launch_workflow_outside_study.py @@ -0,0 +1,23 @@ +from tests.base_test import BaseTest + +from crc import session +from crc.models.user import UserModel +from crc.services.workflow_service import WorkflowService + +from example_data import ExampleDataLoader + + +class TestNoStudyWorkflow(BaseTest): + + def test_no_study_workflow(self): + self.load_example_data() + spec = ExampleDataLoader().create_spec('hello_world', 'Hello World', standalone=True, from_tests=True) + user = session.query(UserModel).first() + self.assertIsNotNone(user) + workflow_model = WorkflowService.get_workflow_from_spec(spec.id, user) + workflow_api = self.get_workflow_api(workflow_model) + first_task = workflow_api.next_task + self.complete_form(workflow_model, first_task, {'name': 'Big Guy'}) + workflow_api = self.get_workflow_api(workflow_model) + second_task = workflow_api.next_task + self.assertEqual(second_task.documentation, 'Hello Big Guy') diff --git a/tests/test_message_event.py b/tests/test_message_event.py index f761871b..3d769095 100644 --- a/tests/test_message_event.py +++ b/tests/test_message_event.py @@ -13,10 +13,10 @@ class TestMessageEvent(BaseTest): # Start the workflow. first_task = self.get_workflow_api(workflow).next_task self.assertEqual('Activity_GetData', first_task.name) - workflow = self.get_workflow_api(workflow) + self.complete_form(workflow, first_task, {'formdata': 'asdf'}) - workflow = self.get_workflow_api(workflow) - self.assertEqual('Activity_HowMany', workflow.next_task.name) + workflow_api = self.get_workflow_api(workflow) + self.assertEqual('Activity_HowMany', workflow_api.next_task.name) # reset the workflow # this ultimately calls crc.api.workflow.set_current_task diff --git a/tests/test_multi_instance_tasks_api.py b/tests/test_multi_instance_tasks_api.py index d4f26297..12036e2d 100644 --- a/tests/test_multi_instance_tasks_api.py +++ b/tests/test_multi_instance_tasks_api.py @@ -67,14 +67,14 @@ class TestMultiinstanceTasksApi(BaseTest): content_type="application/json") self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) - workflow = WorkflowApiSchema().load(json_data) - data = workflow.next_task.data + workflow_api = WorkflowApiSchema().load(json_data) + data = workflow_api.next_task.data data['investigator']['email'] = "dhf8r@virginia.edu" - self.complete_form(workflow, workflow.next_task, data) + self.complete_form(workflow, workflow_api.next_task, data) #tasks = self.get_workflow_api(workflow).user_tasks - workflow = self.get_workflow_api(workflow) - self.assertEqual(WorkflowStatus.complete, workflow.status) + workflow_api = self.get_workflow_api(workflow) + self.assertEqual(WorkflowStatus.complete, workflow_api.status) @patch('crc.services.protocol_builder.requests.get') diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index f2742960..9c6f0b41 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -386,15 +386,15 @@ class TestTasksApi(BaseTest): # Start the workflow. first_task = self.get_workflow_api(workflow).next_task self.complete_form(workflow, first_task, {"has_bananas": True}) - workflow = self.get_workflow_api(workflow) - self.assertEqual('Task_Num_Bananas', workflow.next_task.name) + workflow_api = self.get_workflow_api(workflow) + self.assertEqual('Task_Num_Bananas', workflow_api.next_task.name) # Trying to re-submit the initial task, and answer differently, should result in an error. self.complete_form(workflow, first_task, {"has_bananas": False}, error_code="invalid_state") # Go ahead and set the number of bananas. - workflow = self.get_workflow_api(workflow) - task = workflow.next_task + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task self.complete_form(workflow, task, {"num_bananas": 4}) # We are now at the end of the workflow. @@ -405,19 +405,19 @@ class TestTasksApi(BaseTest): content_type="application/json") self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) - workflow = WorkflowApiSchema().load(json_data) + workflow_api = WorkflowApiSchema().load(json_data) # Assure the Next Task is the one we just reset the token to be on. - self.assertEqual("Task_Has_Bananas", workflow.next_task.name) + self.assertEqual("Task_Has_Bananas", workflow_api.next_task.name) # Go ahead and get that workflow one more time, it should still be right. - workflow = self.get_workflow_api(workflow) + workflow_api = self.get_workflow_api(workflow) # Assure the Next Task is the one we just reset the token to be on. - self.assertEqual("Task_Has_Bananas", workflow.next_task.name) + self.assertEqual("Task_Has_Bananas", workflow_api.next_task.name) # The next task should be a different value. - self.complete_form(workflow, workflow.next_task, {"has_bananas": False}) - workflow = self.get_workflow_api(workflow) - self.assertEqual('Task_Why_No_Bananas', workflow.next_task.name) + self.complete_form(workflow, workflow_api.next_task, {"has_bananas": False}) + workflow_api = self.get_workflow_api(workflow) + self.assertEqual('Task_Why_No_Bananas', workflow_api.next_task.name) diff --git a/tests/workflow/test_workflow_boolean_default.py b/tests/workflow/test_workflow_boolean_default.py index 1773295a..a2b04228 100644 --- a/tests/workflow/test_workflow_boolean_default.py +++ b/tests/workflow/test_workflow_boolean_default.py @@ -7,7 +7,7 @@ class TestBooleanDefault(BaseTest): workflow = self.create_workflow('boolean_default_value') workflow_api = self.get_workflow_api(workflow) set_default_task = workflow_api.next_task - result = self.complete_form(workflow_api, set_default_task, {'yes_no': yes_no}) + result = self.complete_form(workflow, set_default_task, {'yes_no': yes_no}) return result def test_boolean_true_string(self): diff --git a/tests/workflow/test_workflow_enum_default_value_expression.py b/tests/workflow/test_workflow_enum_default_value_expression.py index 00c2abf6..644d52f8 100644 --- a/tests/workflow/test_workflow_enum_default_value_expression.py +++ b/tests/workflow/test_workflow_enum_default_value_expression.py @@ -7,35 +7,35 @@ class TestWorkflowEnumDefault(BaseTest): def test_enum_default_from_value_expression(self): workflow = self.create_workflow('enum_value_expression') - first_task = self.get_workflow_api(workflow).next_task - self.assertEqual('Activity_UserInput', first_task.name) workflow_api = self.get_workflow_api(workflow) + first_task = workflow_api.next_task + self.assertEqual('Activity_UserInput', first_task.name) - result = self.complete_form(workflow_api, first_task, {'user_input': True}) + result = self.complete_form(workflow, first_task, {'user_input': True}) self.assertIn('user_input', result.next_task.data) self.assertEqual(True, result.next_task.data['user_input']) self.assertIn('lookup_output', result.next_task.data) self.assertEqual('black', result.next_task.data['lookup_output']) workflow_api = self.get_workflow_api(workflow) - self.assertEqual('Activity_PickColor', self.get_workflow_api(workflow_api).next_task.name) + self.assertEqual('Activity_PickColor', workflow_api.next_task.name) self.assertEqual({'value': 'black', 'label': 'Black'}, workflow_api.next_task.data['color_select']) # workflow = self.create_workflow('enum_value_expression') - first_task = self.get_workflow_api(workflow).next_task - self.assertEqual('Activity_UserInput', first_task.name) workflow_api = self.get_workflow_api(workflow) + first_task = workflow_api.next_task + self.assertEqual('Activity_UserInput', first_task.name) - result = self.complete_form(workflow_api, first_task, {'user_input': False}) + result = self.complete_form(workflow, first_task, {'user_input': False}) self.assertIn('user_input', result.next_task.data) self.assertEqual(False, result.next_task.data['user_input']) self.assertIn('lookup_output', result.next_task.data) self.assertEqual('white', result.next_task.data['lookup_output']) workflow_api = self.get_workflow_api(workflow) - self.assertEqual('Activity_PickColor', self.get_workflow_api(workflow_api).next_task.name) + self.assertEqual('Activity_PickColor', workflow_api.next_task.name) self.assertEqual({'value': 'white', 'label': 'White'}, workflow_api.next_task.data['color_select']) def test_enum_value_expression_and_default(self): diff --git a/tests/workflow/test_workflow_form_field_name.py b/tests/workflow/test_workflow_form_field_name.py index 8a6aead2..6e38a816 100644 --- a/tests/workflow/test_workflow_form_field_name.py +++ b/tests/workflow/test_workflow_form_field_name.py @@ -18,7 +18,7 @@ class TestFormFieldName(BaseTest): workflow_api = self.get_workflow_api(workflow) first_task = workflow_api.next_task - self.complete_form(workflow_api, first_task, {}) + self.complete_form(workflow, first_task, {}) workflow_api = self.get_workflow_api(workflow) second_task = workflow_api.next_task diff --git a/tests/workflow/test_workflow_hidden_required_field.py b/tests/workflow/test_workflow_hidden_required_field.py index b77dbb24..610c917d 100644 --- a/tests/workflow/test_workflow_hidden_required_field.py +++ b/tests/workflow/test_workflow_hidden_required_field.py @@ -34,14 +34,13 @@ class TestWorkflowHiddenRequiredField(BaseTest): first_task = workflow_api.next_task self.assertEqual('Activity_Hello', first_task.name) - workflow_api = self.get_workflow_api(workflow) - self.complete_form(workflow_api, first_task, {}) + self.complete_form(workflow, first_task, {}) workflow_api = self.get_workflow_api(workflow) second_task = workflow_api.next_task self.assertEqual('Activity_HiddenField', second_task.name) - self.complete_form(workflow_api, second_task, {}) + self.complete_form(workflow, second_task, {}) workflow_api = self.get_workflow_api(workflow) # The color field is hidden and required. Make sure we use the default value diff --git a/tests/workflow/test_workflow_restart.py b/tests/workflow/test_workflow_restart.py index 98cbf78a..2a108d99 100644 --- a/tests/workflow/test_workflow_restart.py +++ b/tests/workflow/test_workflow_restart.py @@ -12,20 +12,20 @@ class TestWorkflowRestart(BaseTest): workflow = self.create_workflow('message_event') - first_task = self.get_workflow_api(workflow).next_task - self.assertEqual('Activity_GetData', first_task.name) workflow_api = self.get_workflow_api(workflow) + first_task = workflow_api.next_task + self.assertEqual('Activity_GetData', first_task.name) - result = self.complete_form(workflow_api, first_task, {'formdata': 'asdf'}) + result = self.complete_form(workflow, first_task, {'formdata': 'asdf'}) self.assertIn('formdata', result.next_task.data) self.assertEqual('asdf', result.next_task.data['formdata']) workflow_api = self.get_workflow_api(workflow) - self.assertEqual('Activity_HowMany', self.get_workflow_api(workflow_api).next_task.name) + self.assertEqual('Activity_HowMany', workflow_api.next_task.name) # restart with data. should land at beginning with data workflow_api = self.restart_workflow_api(result) - first_task = self.get_workflow_api(workflow_api).next_task + first_task = workflow_api.next_task self.assertEqual('Activity_GetData', first_task.name) self.assertIn('formdata', workflow_api.next_task.data) self.assertEqual('asdf', workflow_api.next_task.data['formdata']) @@ -36,7 +36,6 @@ class TestWorkflowRestart(BaseTest): self.assertEqual('Activity_GetData', first_task.name) self.assertNotIn('formdata', workflow_api.next_task.data) - def test_workflow_restart_delete_files(self): self.load_example_data() irb_code = 'Study_Protocol_Document' @@ -80,14 +79,14 @@ class TestWorkflowRestart(BaseTest): study_id = workflow.study_id # Start the workflow. - first_task = self.get_workflow_api(workflow).next_task - self.assertEqual('Activity_GetData', first_task.name) workflow_api = self.get_workflow_api(workflow) - self.complete_form(workflow_api, first_task, {'formdata': 'asdf'}) + first_task = workflow_api.next_task + self.assertEqual('Activity_GetData', first_task.name) + self.complete_form(workflow, first_task, {'formdata': 'asdf'}) workflow_api = self.get_workflow_api(workflow) self.assertEqual('Activity_HowMany', workflow_api.next_task.name) - workflow_api = self.restart_workflow_api(workflow) + self.restart_workflow_api(workflow) study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first() self.assertEqual('New Title', study_result.title) @@ -106,17 +105,16 @@ class TestWorkflowRestart(BaseTest): study_id = workflow.study_id # Start the workflow. - first_task = self.get_workflow_api(workflow).next_task - self.assertEqual('Activity_GetData', first_task.name) workflow_api = self.get_workflow_api(workflow) - self.complete_form(workflow_api, first_task, {'formdata': 'asdf'}) + first_task = workflow_api.next_task + self.assertEqual('Activity_GetData', first_task.name) + self.complete_form(workflow, first_task, {'formdata': 'asdf'}) workflow_api = self.get_workflow_api(workflow) next_task = workflow_api.next_task self.assertEqual('Activity_HowMany', next_task.name) - self.complete_form(workflow_api, next_task, {'how_many': 3}) + self.complete_form(workflow, next_task, {'how_many': 3}) - workflow_api = self.restart_workflow_api(workflow) study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first() self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title) diff --git a/tests/workflow/test_workflow_spec_api.py b/tests/workflow/test_workflow_spec_api.py index d54fbbf1..498ea37c 100644 --- a/tests/workflow/test_workflow_spec_api.py +++ b/tests/workflow/test_workflow_spec_api.py @@ -3,7 +3,9 @@ import json from tests.base_test import BaseTest from crc import session from crc.models.file import FileModel -from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowSpecCategoryModel +from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowSpecCategoryModel, WorkflowSpecCategoryModelSchema + +from example_data import ExampleDataLoader class TestWorkflowSpec(BaseTest): @@ -28,7 +30,8 @@ class TestWorkflowSpec(BaseTest): category_id = session.query(WorkflowSpecCategoryModel).first().id category_count = session.query(WorkflowSpecModel).filter_by(category_id=category_id).count() spec = WorkflowSpecModel(id='make_cookies', name='make_cookies', display_name='Cooooookies', - description='Om nom nom delicious cookies', category_id=category_id) + description='Om nom nom delicious cookies', category_id=category_id, + standalone=False) rv = self.app.post('/v1.0/workflow-specification', headers=self.logged_in_headers(), content_type="application/json", @@ -101,3 +104,60 @@ class TestWorkflowSpec(BaseTest): num_workflows_after = session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id).count() self.assertEqual(num_files_after + num_workflows_after, 0) + def test_get_standalone_workflow_specs(self): + self.load_example_data() + category = session.query(WorkflowSpecCategoryModel).first() + ExampleDataLoader().create_spec('hello_world', 'Hello World', category_id=category.id, + standalone=True, from_tests=True) + rv = self.app.get('/v1.0/workflow-specification/standalone', headers=self.logged_in_headers()) + self.assertEqual(1, len(rv.json)) + + ExampleDataLoader().create_spec('email_script', 'Email Script', category_id=category.id, + standalone=True, from_tests=True) + + rv = self.app.get('/v1.0/workflow-specification/standalone', headers=self.logged_in_headers()) + self.assertEqual(2, len(rv.json)) + + def test_get_workflow_from_workflow_spec(self): + self.load_example_data() + spec = ExampleDataLoader().create_spec('hello_world', 'Hello World', standalone=True, from_tests=True) + rv = self.app.post(f'/v1.0/workflow-specification/{spec.id}', headers=self.logged_in_headers()) + self.assert_success(rv) + self.assertEqual('hello_world', rv.json['workflow_spec_id']) + self.assertEqual('Task_GetName', rv.json['next_task']['name']) + + def test_add_workflow_spec_category(self): + self.load_example_data() + count = session.query(WorkflowSpecCategoryModel).count() + category = WorkflowSpecCategoryModel( + id=count, + name='another_test_category', + display_name='Another Test Category', + display_order=0 + ) + rv = self.app.post(f'/v1.0/workflow-specification-category', + headers=self.logged_in_headers(), + content_type="application/json", + data=json.dumps(WorkflowSpecCategoryModelSchema().dump(category)) + ) + self.assert_success(rv) + result = session.query(WorkflowSpecCategoryModel).filter(WorkflowSpecCategoryModel.name=='another_test_category').first() + self.assertEqual('Another Test Category', result.display_name) + self.assertEqual(count, result.id) + + def test_update_workflow_spec_category(self): + self.load_example_data() + category = session.query(WorkflowSpecCategoryModel).first() + category_name_before = category.name + new_category_name = category_name_before + '_asdf' + self.assertNotEqual(category_name_before, new_category_name) + + category.name = new_category_name + + rv = self.app.put(f'/v1.0/workflow-specification-category/{category.id}', + content_type="application/json", + headers=self.logged_in_headers(), + data=json.dumps(WorkflowSpecCategoryModelSchema().dump(category))) + self.assert_success(rv) + json_data = json.loads(rv.get_data(as_text=True)) + self.assertEqual(new_category_name, json_data['name']) diff --git a/tests/workflow/test_workflow_value_expression.py b/tests/workflow/test_workflow_value_expression.py index d527c694..47751b1c 100644 --- a/tests/workflow/test_workflow_value_expression.py +++ b/tests/workflow/test_workflow_value_expression.py @@ -9,7 +9,7 @@ class TestValueExpression(BaseTest): workflow_api = self.get_workflow_api(workflow) first_task = workflow_api.next_task - self.complete_form(workflow_api, first_task, {'value_expression_value': ''}) + self.complete_form(workflow, first_task, {'value_expression_value': ''}) workflow_api = self.get_workflow_api(workflow) second_task = workflow_api.next_task @@ -26,7 +26,7 @@ class TestValueExpression(BaseTest): workflow_api = self.get_workflow_api(workflow) first_task = workflow_api.next_task - self.complete_form(workflow_api, first_task, {'value_expression_value': 'black'}) + self.complete_form(workflow, first_task, {'value_expression_value': 'black'}) workflow_api = self.get_workflow_api(workflow) second_task = workflow_api.next_task