Merge pull request #296 from sartography/launch-workflow-outside-study-204

Launch workflow outside study 204
This commit is contained in:
Dan Funk 2021-05-04 11:16:08 -04:00 committed by GitHub
commit 77d9bfca43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 343 additions and 99 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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'))

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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 ")

View File

@ -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)

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_0ixyfs0" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_HelloWorld" name="Hello World Process" isExecutable="true">
<bpmn:documentation>This workflow asks for a name and says hello</bpmn:documentation>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0qyd2b7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0qyd2b7" sourceRef="StartEvent_1" targetRef="Task_GetName" />
<bpmn:userTask id="Task_GetName" name="Get Name" camunda:formKey="Name">
<bpmn:documentation>Hello</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="name" label="Name" type="string" defaultValue="World" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0qyd2b7</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1h46b40</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1h46b40" sourceRef="Task_GetName" targetRef="Task_SayHello" />
<bpmn:manualTask id="Task_SayHello" name="Say Hello">
<bpmn:documentation>Hello {{name}}</bpmn:documentation>
<bpmn:incoming>SequenceFlow_1h46b40</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0lqrc6e</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="EndEvent_1l03lqw">
<bpmn:incoming>SequenceFlow_0lqrc6e</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0lqrc6e" sourceRef="Task_SayHello" targetRef="EndEvent_1l03lqw" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_HelloWorld">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0qyd2b7_di" bpmnElement="SequenceFlow_0qyd2b7">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_0fbucz7_di" bpmnElement="Task_GetName">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1h46b40_di" bpmnElement="SequenceFlow_1h46b40">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ManualTask_1tia2zr_di" bpmnElement="Task_SayHello">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1l03lqw_di" bpmnElement="EndEvent_1l03lqw">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0lqrc6e_di" bpmnElement="SequenceFlow_0lqrc6e">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -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)

View File

@ -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",

View File

@ -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'})

View File

@ -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')

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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'])

View File

@ -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