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 - name: spec_id
in: path in: path
required: true required: true
description: The unique id of an existing workflow specification to modify. description: The unique id of an existing workflow specification.
schema: schema:
type: string type: string
get: get:
@ -440,6 +440,18 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/WorkflowSpec" $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: put:
operationId: crc.api.workflow.update_workflow_specification operationId: crc.api.workflow.update_workflow_specification
security: security:
@ -469,6 +481,21 @@ paths:
responses: responses:
'204': '204':
description: The workflow specification has been removed. 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: /workflow-specification/{spec_id}/validate:
parameters: parameters:
- name: spec_id - name: spec_id
@ -1536,6 +1563,9 @@ components:
category_id: category_id:
type: integer type: integer
nullable: true nullable: true
standalone:
type: boolean
example: false
workflow_spec_category: workflow_spec_category:
$ref: "#/components/schemas/WorkflowSpecCategory" $ref: "#/components/schemas/WorkflowSpecCategory"
is_status: is_status:
@ -1608,6 +1638,8 @@ components:
type: integer type: integer
num_tasks_incomplete: num_tasks_incomplete:
type: integer type: integer
study_id:
type: integer
example: example:
id: 291234 id: 291234

View File

@ -101,6 +101,24 @@ def delete_workflow_specification(spec_id):
session.commit() 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): def get_workflow(workflow_id, do_engine_steps=True):
"""Retrieve workflow based on workflow_id, and return it in the last saved State. """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. """ 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: if workflow_model is None:
raise ApiError("invalid_workflow_id", "The given workflow id is not valid.", status_code=404) 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) processor = WorkflowProcessor(workflow_model)
task_id = uuid.UUID(task_id) task_id = uuid.UUID(task_id)
spiff_task = processor.bpmn_workflow.get_task(task_id) spiff_task = processor.bpmn_workflow.get_task(task_id)

View File

@ -191,7 +191,7 @@ class DocumentDirectory(object):
class WorkflowApi(object): class WorkflowApi(object):
def __init__(self, id, status, next_task, navigation, def __init__(self, id, status, next_task, navigation,
spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, 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.id = id
self.status = status self.status = status
self.next_task = next_task # The next task that requires user input. 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.last_updated = last_updated
self.title = title self.title = title
self.is_review = is_review self.is_review = is_review
self.study_id = study_id or ''
class WorkflowApiSchema(ma.Schema): class WorkflowApiSchema(ma.Schema):
class Meta: class Meta:
model = WorkflowApi model = WorkflowApi
fields = ["id", "status", "next_task", "navigation", fields = ["id", "status", "next_task", "navigation",
"workflow_spec_id", "spec_version", "is_latest_spec", "total_tasks", "completed_tasks", "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 unknown = INCLUDE
status = EnumField(WorkflowStatus) status = EnumField(WorkflowStatus)
@ -221,7 +222,7 @@ class WorkflowApiSchema(ma.Schema):
def make_workflow(self, data, **kwargs): def make_workflow(self, data, **kwargs):
keys = ['id', 'status', 'next_task', 'navigation', keys = ['id', 'status', 'next_task', 'navigation',
'workflow_spec_id', 'spec_version', 'is_latest_spec', "total_tasks", "completed_tasks", '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 = {key: data[key] for key in keys}
filtered_fields['next_task'] = TaskSchema().make_task(data['next_task']) filtered_fields['next_task'] = TaskSchema().make_task(data['next_task'])
return WorkflowApi(**filtered_fields) return WorkflowApi(**filtered_fields)

View File

@ -10,7 +10,7 @@ from crc.services.ldap_service import LdapService
class TaskEventModel(db.Model): class TaskEventModel(db.Model):
__tablename__ = 'task_event' __tablename__ = 'task_event'
id = db.Column(db.Integer, primary_key=True) 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. 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_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False)
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id')) 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_id = db.Column(db.Integer, db.ForeignKey('workflow_spec_category.id'), nullable=True)
category = db.relationship("WorkflowSpecCategoryModel") category = db.relationship("WorkflowSpecCategoryModel")
is_master_spec = db.Column(db.Boolean, default=False) is_master_spec = db.Column(db.Boolean, default=False)
standalone = db.Column(db.Boolean, default=False)
class WorkflowSpecModelSchema(SQLAlchemyAutoSchema): class WorkflowSpecModelSchema(SQLAlchemyAutoSchema):
@ -88,6 +89,7 @@ class WorkflowModel(db.Model):
total_tasks = db.Column(db.Integer, default=0) total_tasks = db.Column(db.Integer, default=0)
completed_tasks = db.Column(db.Integer, default=0) completed_tasks = db.Column(db.Integer, default=0)
last_updated = db.Column(db.DateTime) last_updated = db.Column(db.DateTime)
user_id = db.Column(db.String, default=None)
# Order By is important or generating hashes on reviews. # Order By is important or generating hashes on reviews.
dependencies = db.relationship(WorkflowSpecDependencyFile, cascade="all, delete, delete-orphan", dependencies = db.relationship(WorkflowSpecDependencyFile, cascade="all, delete, delete-orphan",
order_by="WorkflowSpecDependencyFile.file_data_id") order_by="WorkflowSpecDependencyFile.file_data_id")

View File

@ -495,6 +495,7 @@ class StudyService(object):
def _create_workflow_model(study: StudyModel, spec): def _create_workflow_model(study: StudyModel, spec):
workflow_model = WorkflowModel(status=WorkflowStatus.not_started, workflow_model = WorkflowModel(status=WorkflowStatus.not_started,
study=study, study=study,
user_id=None,
workflow_spec_id=spec.id, workflow_spec_id=spec.id,
last_updated=datetime.now()) last_updated=datetime.now())
session.add(workflow_model) session.add(workflow_model)

View File

@ -408,7 +408,8 @@ class WorkflowService(object):
completed_tasks=processor.workflow_model.completed_tasks, completed_tasks=processor.workflow_model.completed_tasks,
last_updated=processor.workflow_model.last_updated, last_updated=processor.workflow_model.last_updated,
is_review=is_review, 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. 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. # This may or may not work, sometimes there is no next task to complete.
@ -667,30 +668,39 @@ class WorkflowService(object):
@staticmethod @staticmethod
def get_users_assigned_to_task(processor, spiff_task) -> List[str]: 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: if processor.workflow_model.study_id is None and processor.workflow_model.user_id is None:
associated = StudyService.get_study_associates(processor.workflow_model.study.id) raise ApiError.from_task(code='invalid_workflow',
return [user['uid'] for user in associated if user['access']] message='A workflow must have either a study_id or a user_id.',
if spiff_task.task_spec.lane not in spiff_task.data: task=spiff_task)
return [] # No users are assignable to the task at this moment # Standalone workflow - we only care about the current user
lane_users = spiff_task.data[spiff_task.task_spec.lane] elif processor.workflow_model.study_id is None and processor.workflow_model.user_id is not None:
if not isinstance(lane_users, list): return [processor.workflow_model.user_id]
lane_users = [lane_users] # 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 = [] lane_uids = []
for user in lane_users: for user in lane_users:
if isinstance(user, dict): if isinstance(user, dict):
if 'value' in user and user['value'] is not None: if 'value' in user and user['value'] is not None:
lane_uids.append(user['value']) 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: 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." % 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) 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)
return lane_uids return lane_uids
@staticmethod @staticmethod
def log_task_action(user_uid, processor, spiff_task, action): def log_task_action(user_uid, processor, spiff_task, action):
@ -783,3 +793,19 @@ class WorkflowService(object):
for workflow in workflows: for workflow in workflows:
if workflow.status == WorkflowStatus.user_input_required or workflow.status == WorkflowStatus.waiting: if workflow.status == WorkflowStatus.user_input_required or workflow.status == WorkflowStatus.waiting:
WorkflowProcessor.reset(workflow, clear_data=False) 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) from_tests=True)
def create_spec(self, id, name, display_name="", description="", filepath=None, master_spec=False, 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. """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. 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.""" returns an array of data models to be added to the database."""
@ -278,7 +278,8 @@ class ExampleDataLoader:
description=description, description=description,
is_master_spec=master_spec, is_master_spec=master_spec,
category_id=category_id, category_id=category_id,
display_order=display_order) display_order=display_order,
standalone=standalone)
db.session.add(spec) db.session.add(spec)
db.session.commit() db.session.commit()
if not filepath and not from_tests: 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() specs = session.query(WorkflowSpecModel).all()
self.assertIsNotNone(specs) 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: for spec in specs:
files = session.query(FileModel).filter_by(workflow_spec_id=spec.id).all() files = session.query(FileModel).filter_by(workflow_spec_id=spec.id).all()
self.assertIsNotNone(files) 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, def complete_form(self, workflow_in, task_in, dict_data, update_all=False, error_code=None, terminate_loop=None,
user_uid="dhf8r"): 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 prev_completed_task_count = workflow_in.completed_tasks
if isinstance(task_in, dict): if isinstance(task_in, dict):
task_id = task_in["id"] task_id = task_in["id"]
@ -421,7 +420,8 @@ class BaseTest(unittest.TestCase):
.order_by(TaskEventModel.date.desc()).all() .order_by(TaskEventModel.date.desc()).all()
self.assertGreater(len(task_events), 0) self.assertGreater(len(task_events), 0)
event = 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(user_uid, event.user_uid)
self.assertEqual(workflow.id, event.workflow_id) self.assertEqual(workflow.id, event.workflow_id)
self.assertEqual(workflow.workflow_spec_id, event.workflow_spec_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, study_id = self.load_workflow()
workflow_api, first_task = self.get_first_task(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) study_result = self.put_study_on_hold(study_id)
self.assertEqual('New Title', study_result.title) self.assertEqual('New Title', study_result.title)
@ -82,10 +82,10 @@ class TestStudyCancellations(BaseTest):
workflow, study_id = self.load_workflow() workflow, study_id = self.load_workflow()
workflow_api, first_task = self.get_first_task(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) 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) study_result = self.put_study_on_hold(study_id)
self.assertEqual('Second Title', study_result.title) self.assertEqual('Second Title', study_result.title)
@ -95,13 +95,13 @@ class TestStudyCancellations(BaseTest):
workflow, study_id = self.load_workflow() workflow, study_id = self.load_workflow()
workflow_api, first_task = self.get_first_task(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) 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) 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) study_result = self.put_study_on_hold(study_id)
self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title) 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 category_id = session.query(WorkflowSpecCategoryModel).first().id
# Add a workflow spec # Add a workflow spec
spec = WorkflowSpecModel(id='make_cookies', name='make_cookies', display_name='Cooooookies', 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', rv = self.app.post('/v1.0/workflow-specification',
headers=self.logged_in_headers(), headers=self.logged_in_headers(),
content_type="application/json", content_type="application/json",

View File

@ -23,7 +23,6 @@ class TestEmailScript(BaseTest):
first_task = self.get_workflow_api(workflow).next_task 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.complete_form(workflow, first_task, {'subject': 'My Email Subject', 'recipients': 'test@example.com'})
self.assertEqual(1, len(outbox)) self.assertEqual(1, len(outbox))
@ -49,7 +48,6 @@ class TestEmailScript(BaseTest):
def test_bad_email_address_1(self): def test_bad_email_address_1(self):
workflow = self.create_workflow('email_script') workflow = self.create_workflow('email_script')
first_task = self.get_workflow_api(workflow).next_task first_task = self.get_workflow_api(workflow).next_task
workflow = self.get_workflow_api(workflow)
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
self.complete_form(workflow, first_task, {'recipients': 'test@example'}) self.complete_form(workflow, first_task, {'recipients': 'test@example'})
@ -57,7 +55,6 @@ class TestEmailScript(BaseTest):
def test_bad_email_address_2(self): def test_bad_email_address_2(self):
workflow = self.create_workflow('email_script') workflow = self.create_workflow('email_script')
first_task = self.get_workflow_api(workflow).next_task first_task = self.get_workflow_api(workflow).next_task
workflow = self.get_workflow_api(workflow)
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
self.complete_form(workflow, first_task, {'recipients': 'test'}) 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. # Start the workflow.
first_task = self.get_workflow_api(workflow).next_task first_task = self.get_workflow_api(workflow).next_task
self.assertEqual('Activity_GetData', first_task.name) self.assertEqual('Activity_GetData', first_task.name)
workflow = self.get_workflow_api(workflow)
self.complete_form(workflow, first_task, {'formdata': 'asdf'}) self.complete_form(workflow, first_task, {'formdata': 'asdf'})
workflow = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
self.assertEqual('Activity_HowMany', workflow.next_task.name) self.assertEqual('Activity_HowMany', workflow_api.next_task.name)
# reset the workflow # reset the workflow
# this ultimately calls crc.api.workflow.set_current_task # this ultimately calls crc.api.workflow.set_current_task

View File

@ -67,14 +67,14 @@ class TestMultiinstanceTasksApi(BaseTest):
content_type="application/json") content_type="application/json")
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))
workflow = WorkflowApiSchema().load(json_data) workflow_api = WorkflowApiSchema().load(json_data)
data = workflow.next_task.data data = workflow_api.next_task.data
data['investigator']['email'] = "dhf8r@virginia.edu" 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 #tasks = self.get_workflow_api(workflow).user_tasks
workflow = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
self.assertEqual(WorkflowStatus.complete, workflow.status) self.assertEqual(WorkflowStatus.complete, workflow_api.status)
@patch('crc.services.protocol_builder.requests.get') @patch('crc.services.protocol_builder.requests.get')

View File

@ -386,15 +386,15 @@ class TestTasksApi(BaseTest):
# Start the workflow. # Start the workflow.
first_task = self.get_workflow_api(workflow).next_task first_task = self.get_workflow_api(workflow).next_task
self.complete_form(workflow, first_task, {"has_bananas": True}) self.complete_form(workflow, first_task, {"has_bananas": True})
workflow = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
self.assertEqual('Task_Num_Bananas', workflow.next_task.name) 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. # 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") self.complete_form(workflow, first_task, {"has_bananas": False}, error_code="invalid_state")
# Go ahead and set the number of bananas. # Go ahead and set the number of bananas.
workflow = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
task = workflow.next_task task = workflow_api.next_task
self.complete_form(workflow, task, {"num_bananas": 4}) self.complete_form(workflow, task, {"num_bananas": 4})
# We are now at the end of the workflow. # We are now at the end of the workflow.
@ -405,19 +405,19 @@ class TestTasksApi(BaseTest):
content_type="application/json") content_type="application/json")
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) 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. # 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. # 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. # 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. # The next task should be a different value.
self.complete_form(workflow, workflow.next_task, {"has_bananas": False}) self.complete_form(workflow, workflow_api.next_task, {"has_bananas": False})
workflow = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
self.assertEqual('Task_Why_No_Bananas', workflow.next_task.name) 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 = self.create_workflow('boolean_default_value')
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
set_default_task = workflow_api.next_task 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 return result
def test_boolean_true_string(self): def test_boolean_true_string(self):

View File

@ -7,35 +7,35 @@ class TestWorkflowEnumDefault(BaseTest):
def test_enum_default_from_value_expression(self): def test_enum_default_from_value_expression(self):
workflow = self.create_workflow('enum_value_expression') 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) 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.assertIn('user_input', result.next_task.data)
self.assertEqual(True, result.next_task.data['user_input']) self.assertEqual(True, result.next_task.data['user_input'])
self.assertIn('lookup_output', result.next_task.data) self.assertIn('lookup_output', result.next_task.data)
self.assertEqual('black', result.next_task.data['lookup_output']) self.assertEqual('black', result.next_task.data['lookup_output'])
workflow_api = self.get_workflow_api(workflow) 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']) self.assertEqual({'value': 'black', 'label': 'Black'}, workflow_api.next_task.data['color_select'])
# #
workflow = self.create_workflow('enum_value_expression') 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) 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.assertIn('user_input', result.next_task.data)
self.assertEqual(False, result.next_task.data['user_input']) self.assertEqual(False, result.next_task.data['user_input'])
self.assertIn('lookup_output', result.next_task.data) self.assertIn('lookup_output', result.next_task.data)
self.assertEqual('white', result.next_task.data['lookup_output']) self.assertEqual('white', result.next_task.data['lookup_output'])
workflow_api = self.get_workflow_api(workflow) 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']) self.assertEqual({'value': 'white', 'label': 'White'}, workflow_api.next_task.data['color_select'])
def test_enum_value_expression_and_default(self): def test_enum_value_expression_and_default(self):

View File

@ -18,7 +18,7 @@ class TestFormFieldName(BaseTest):
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
first_task = workflow_api.next_task 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) workflow_api = self.get_workflow_api(workflow)
second_task = workflow_api.next_task second_task = workflow_api.next_task

View File

@ -34,14 +34,13 @@ class TestWorkflowHiddenRequiredField(BaseTest):
first_task = workflow_api.next_task first_task = workflow_api.next_task
self.assertEqual('Activity_Hello', first_task.name) 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) workflow_api = self.get_workflow_api(workflow)
second_task = workflow_api.next_task second_task = workflow_api.next_task
self.assertEqual('Activity_HiddenField', second_task.name) 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) workflow_api = self.get_workflow_api(workflow)
# The color field is hidden and required. Make sure we use the default value # 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') 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) 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.assertIn('formdata', result.next_task.data)
self.assertEqual('asdf', result.next_task.data['formdata']) self.assertEqual('asdf', result.next_task.data['formdata'])
workflow_api = self.get_workflow_api(workflow) 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 # restart with data. should land at beginning with data
workflow_api = self.restart_workflow_api(result) 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.assertEqual('Activity_GetData', first_task.name)
self.assertIn('formdata', workflow_api.next_task.data) self.assertIn('formdata', workflow_api.next_task.data)
self.assertEqual('asdf', workflow_api.next_task.data['formdata']) self.assertEqual('asdf', workflow_api.next_task.data['formdata'])
@ -36,7 +36,6 @@ class TestWorkflowRestart(BaseTest):
self.assertEqual('Activity_GetData', first_task.name) self.assertEqual('Activity_GetData', first_task.name)
self.assertNotIn('formdata', workflow_api.next_task.data) self.assertNotIn('formdata', workflow_api.next_task.data)
def test_workflow_restart_delete_files(self): def test_workflow_restart_delete_files(self):
self.load_example_data() self.load_example_data()
irb_code = 'Study_Protocol_Document' irb_code = 'Study_Protocol_Document'
@ -80,14 +79,14 @@ class TestWorkflowRestart(BaseTest):
study_id = workflow.study_id study_id = workflow.study_id
# Start the workflow. # 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) 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) workflow_api = self.get_workflow_api(workflow)
self.assertEqual('Activity_HowMany', workflow_api.next_task.name) 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() study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first()
self.assertEqual('New Title', study_result.title) self.assertEqual('New Title', study_result.title)
@ -106,17 +105,16 @@ class TestWorkflowRestart(BaseTest):
study_id = workflow.study_id study_id = workflow.study_id
# Start the workflow. # 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) 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) workflow_api = self.get_workflow_api(workflow)
next_task = workflow_api.next_task next_task = workflow_api.next_task
self.assertEqual('Activity_HowMany', next_task.name) 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() study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first()
self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title) 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 tests.base_test import BaseTest
from crc import session from crc import session
from crc.models.file import FileModel 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): class TestWorkflowSpec(BaseTest):
@ -28,7 +30,8 @@ class TestWorkflowSpec(BaseTest):
category_id = session.query(WorkflowSpecCategoryModel).first().id category_id = session.query(WorkflowSpecCategoryModel).first().id
category_count = session.query(WorkflowSpecModel).filter_by(category_id=category_id).count() category_count = session.query(WorkflowSpecModel).filter_by(category_id=category_id).count()
spec = WorkflowSpecModel(id='make_cookies', name='make_cookies', display_name='Cooooookies', 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', rv = self.app.post('/v1.0/workflow-specification',
headers=self.logged_in_headers(), headers=self.logged_in_headers(),
content_type="application/json", 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() num_workflows_after = session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id).count()
self.assertEqual(num_files_after + num_workflows_after, 0) 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) workflow_api = self.get_workflow_api(workflow)
first_task = workflow_api.next_task 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) workflow_api = self.get_workflow_api(workflow)
second_task = workflow_api.next_task second_task = workflow_api.next_task
@ -26,7 +26,7 @@ class TestValueExpression(BaseTest):
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
first_task = workflow_api.next_task 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) workflow_api = self.get_workflow_api(workflow)
second_task = workflow_api.next_task second_task = workflow_api.next_task