Merge pull request #264 from sartography/bug/243_complete_all

Bug/243 complete all
This commit is contained in:
Dan Funk 2021-03-14 12:34:55 -04:00 committed by GitHub
commit 14386b8ba9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 310 additions and 174 deletions

View File

@ -904,6 +904,12 @@ paths:
description: Terminate the loop on a looping task
schema:
type: boolean
- name: update_all
in: query
required: false
description: In the case of a multi-instance user task, update all tasks with the submitted values.
schema:
type: boolean
put:
operationId: crc.api.workflow.update_task
summary: Exclusively for User Tasks, submits form data as a flat set of key/values.

View File

@ -179,7 +179,7 @@ def set_current_task(workflow_id, task_id):
return WorkflowApiSchema().dump(workflow_api_model)
def update_task(workflow_id, task_id, body, terminate_loop=None):
def update_task(workflow_id, task_id, body, terminate_loop=None, update_all=False):
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
if workflow_model is None:
raise ApiError("invalid_workflow_id", "The given workflow id is not valid.", status_code=404)
@ -191,6 +191,8 @@ def update_task(workflow_id, task_id, body, terminate_loop=None):
task_id = uuid.UUID(task_id)
spiff_task = processor.bpmn_workflow.get_task(task_id)
_verify_user_and_role(processor, spiff_task)
user = UserService.current_user(allow_admin_impersonate=False) # Always log as the real user.
if not spiff_task:
raise ApiError("empty_task", "Processor failed to obtain task.", status_code=404)
if spiff_task.state != spiff_task.READY:
@ -199,20 +201,39 @@ def update_task(workflow_id, task_id, body, terminate_loop=None):
if terminate_loop:
spiff_task.terminate_loop()
spiff_task.update_data(body)
processor.complete_task(spiff_task)
processor.do_engine_steps()
processor.save()
# Log the action, and any pending task assignments in the event of lanes in the workflow.
user = UserService.current_user(allow_admin_impersonate=False) # Always log as the real user.
WorkflowService.log_task_action(user.uid, processor, spiff_task, WorkflowService.TASK_ACTION_COMPLETE)
# Extract the details specific to the form submitted
form_data = WorkflowService().extract_form_data(body, spiff_task)
# Update the task
__update_task(processor, spiff_task, form_data, user)
# If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
# task spec, complete that form as well.
if update_all:
last_index = spiff_task.task_info()["mi_index"]
next_task = processor.next_task()
while next_task and next_task.task_info()["mi_index"] > last_index:
__update_task(processor, next_task, form_data, user)
last_index = next_task.task_info()["mi_index"]
next_task = processor.next_task()
WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
return WorkflowApiSchema().dump(workflow_api_model)
def __update_task(processor, task, data, user):
"""All the things that need to happen when we complete a form, abstracted
here because we need to do it multiple times when completing all tasks in
a multi-instance task"""
task.update_data(data)
processor.complete_task(task)
processor.do_engine_steps()
processor.save()
WorkflowService.log_task_action(user.uid, processor, task, WorkflowService.TASK_ACTION_COMPLETE)
def list_workflow_spec_categories():
schema = WorkflowSpecCategoryModelSchema(many=True)
return schema.dump(session.query(WorkflowSpecCategoryModel).all())

View File

@ -16,6 +16,7 @@ from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.specs import CancelTask, StartTask, MultiChoice
from SpiffWorkflow.util.deep_merge import DeepMerge
from box import Box
from jinja2 import Template
from crc import db, app
@ -709,7 +710,8 @@ class WorkflowService(object):
@staticmethod
def extract_form_data(latest_data, task):
"""Removes data from latest_data that would be added by the child task or any of its children."""
"""Extracts data from the latest_data that is directly related to the form that is being
submitted."""
data = {}
if hasattr(task.task_spec, 'form'):
@ -721,16 +723,49 @@ class WorkflowService(object):
group = field.get_property(Task.FIELD_PROP_REPEAT)
if group in latest_data:
data[group] = latest_data[group]
elif isinstance(task.task_spec, MultiInstanceTask):
group = task.task_spec.elementVar
if group in latest_data:
data[group] = latest_data[group]
else:
if field.id in latest_data:
data[field.id] = latest_data[field.id]
value = WorkflowService.get_dot_value(field.id, latest_data)
if value is not None:
WorkflowService.set_dot_value(field.id, value, data)
return data
@staticmethod
def get_dot_value(path, source):
### Given a path in dot notation, uas as 'fruit.type' tries to find that value in
### the source, but looking deep in the dictionary.
paths = path.split(".") # [a,b,c]
s = source
index = 0
for p in paths:
index += 1
if isinstance(s, dict) and p in s:
if index == len(paths):
return s[p]
else:
s = s[p]
if path in source:
return source[path]
return None
@staticmethod
def set_dot_value(path, value, target):
### Given a path in dot notation, such as "fruit.type", and a value "apple", will
### set the value in the target dictionary, as target["fruit"]["type"]="apple"
destination = target
paths = path.split(".") # [a,b,c]
index = 0
for p in paths:
index += 1
if p not in destination:
if index == len(paths):
destination[p] = value
else:
destination[p] = {}
destination = destination[p]
return target
@staticmethod
def process_workflows_for_cancels(study_id):
workflows = db.session.query(WorkflowModel).filter_by(study_id=study_id).all()

View File

@ -5,7 +5,7 @@ services:
volumes:
- $HOME/docker/volumes/postgres:/var/lib/postgresql/data
ports:
- 5003:5432
- 5432:5432
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASS}

View File

@ -68,24 +68,24 @@ class BaseTest(unittest.TestCase):
studies = [
{
'id':0,
'title':'The impact of fried pickles on beer consumption in bipedal software developers.',
'last_updated':datetime.datetime.now(),
'status':StudyStatus.in_progress,
'primary_investigator_id':'dhf8r',
'sponsor':'Sartography Pharmaceuticals',
'ind_number':'1234',
'user_uid':'dhf8r'
'id': 0,
'title': 'The impact of fried pickles on beer consumption in bipedal software developers.',
'last_updated': datetime.datetime.now(),
'status': StudyStatus.in_progress,
'primary_investigator_id': 'dhf8r',
'sponsor': 'Sartography Pharmaceuticals',
'ind_number': '1234',
'user_uid': 'dhf8r'
},
{
'id':1,
'title':'Requirement of hippocampal neurogenesis for the behavioral effects of soft pretzels',
'last_updated':datetime.datetime.now(),
'status':StudyStatus.in_progress,
'primary_investigator_id':'dhf8r',
'sponsor':'Makerspace & Co.',
'ind_number':'5678',
'user_uid':'dhf8r'
'id': 1,
'title': 'Requirement of hippocampal neurogenesis for the behavioral effects of soft pretzels',
'last_updated': datetime.datetime.now(),
'status': StudyStatus.in_progress,
'primary_investigator_id': 'dhf8r',
'sponsor': 'Makerspace & Co.',
'ind_number': '5678',
'user_uid': 'dhf8r'
}
]
@ -141,7 +141,6 @@ class BaseTest(unittest.TestCase):
session.execute("delete from workflow; delete from file_data; delete from file; delete from workflow_spec;")
session.commit()
def load_example_data(self, use_crc_data=False, use_rrt_data=False):
"""use_crc_data will cause this to load the mammoth collection of documents
we built up developing crc, use_rrt_data will do the same for hte rrt project,
@ -219,7 +218,6 @@ class BaseTest(unittest.TestCase):
data = myfile.read()
return data
def assert_success(self, rv, msg=""):
try:
data = json.loads(rv.get_data(as_text=True))
@ -361,7 +359,7 @@ class BaseTest(unittest.TestCase):
def get_workflow_api(self, workflow, do_engine_steps=True, user_uid="dhf8r"):
user = session.query(UserModel).filter_by(uid=user_uid).first()
self.assertIsNotNone(user)
url = (f'/v1.0/workflow/{workflow.id}'
url = (f'/v1.0/workflow/{workflow.id}'
f'?do_engine_steps={str(do_engine_steps)}')
workflow_api = self.get_workflow_common(url, user)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
@ -370,13 +368,14 @@ class BaseTest(unittest.TestCase):
def restart_workflow_api(self, workflow, clear_data=False, user_uid="dhf8r"):
user = session.query(UserModel).filter_by(uid=user_uid).first()
self.assertIsNotNone(user)
url = (f'/v1.0/workflow/{workflow.id}/restart'
url = (f'/v1.0/workflow/{workflow.id}/restart'
f'?clear_data={str(clear_data)}')
workflow_api = self.get_workflow_common(url, user)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
return workflow_api
def complete_form(self, workflow_in, task_in, dict_data, error_code=None, terminate_loop=None, user_uid="dhf8r"):
def complete_form(self, workflow_in, task_in, dict_data, update_all=False, error_code=None, terminate_loop=None,
user_uid="dhf8r"):
prev_completed_task_count = workflow_in.completed_tasks
if isinstance(task_in, dict):
task_id = task_in["id"]
@ -385,16 +384,16 @@ class BaseTest(unittest.TestCase):
user = session.query(UserModel).filter_by(uid=user_uid).first()
self.assertIsNotNone(user)
args = ""
if terminate_loop:
rv = self.app.put('/v1.0/workflow/%i/task/%s/data?terminate_loop=true' % (workflow_in.id, task_id),
headers=self.logged_in_headers(user=user),
content_type="application/json",
data=json.dumps(dict_data))
else:
rv = self.app.put('/v1.0/workflow/%i/task/%s/data' % (workflow_in.id, task_id),
headers=self.logged_in_headers(user=user),
content_type="application/json",
data=json.dumps(dict_data))
args += "?terminate_loop=true"
if update_all:
args += "?update_all=true"
rv = self.app.put('/v1.0/workflow/%i/task/%s/data%s' % (workflow_in.id, task_id, args),
headers=self.logged_in_headers(user=user),
content_type="application/json",
data=json.dumps(dict_data))
if error_code:
self.assert_failure(rv, error_code=error_code)
return
@ -408,7 +407,7 @@ class BaseTest(unittest.TestCase):
# branches may be pruned. As we hit parallel Multi-Instance new tasks may be created...
self.assertIsNotNone(workflow.total_tasks)
# presumably, we also need to deal with sequential items here too . .
if not task_in.multi_instance_type == 'looping':
if not task_in.multi_instance_type == 'looping' and not update_all:
self.assertEqual(prev_completed_task_count + 1, workflow.completed_tasks)
# Assure a record exists in the Task Events

View File

@ -1,5 +1,5 @@
<?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:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1xiske1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<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:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1xiske1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_TestBooleanDefault" name="Test Boolean Default" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1x41riu</bpmn:outgoing>
@ -20,8 +20,7 @@
<bpmn:sequenceFlow id="Flow_0m31ypa" sourceRef="Activity_SelectBoolean" targetRef="Activity_GoodBye" />
<bpmn:manualTask id="Activity_GoodBye" name="Good Bye">
<bpmn:documentation>&lt;H1&gt;Good Bye&lt;/H1&gt;
&lt;div&gt;&lt;span&gt;Pick One: {% if pick_one %}{{ pick_one}}{% endif %} &lt;/span&gt;&lt;/div&gt;
</bpmn:documentation>
&lt;div&gt;&lt;span&gt;Pick One: {% if pick_one %}{{ pick_one}}{% endif %} &lt;/span&gt;&lt;/div&gt;</bpmn:documentation>
<bpmn:incoming>Flow_0m31ypa</bpmn:incoming>
<bpmn:outgoing>Flow_0f3gndz</bpmn:outgoing>
</bpmn:manualTask>
@ -29,13 +28,7 @@
<bpmn:incoming>Flow_0f3gndz</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0f3gndz" sourceRef="Activity_GoodBye" targetRef="Event_0rgpb6o" />
<bpmn:sequenceFlow id="Flow_1x41riu" sourceRef="StartEvent_1" targetRef="Activity_Hello" />
<bpmn:manualTask id="Activity_Hello" name="Hello">
<bpmn:documentation>&lt;H1&gt;Hello&lt;/H1&gt;</bpmn:documentation>
<bpmn:incoming>Flow_1x41riu</bpmn:incoming>
<bpmn:outgoing>Flow_1i32jb7</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_1i32jb7" sourceRef="Activity_Hello" targetRef="Activity_PreData" />
<bpmn:sequenceFlow id="Flow_1x41riu" sourceRef="StartEvent_1" targetRef="Set_Default" />
<bpmn:sequenceFlow id="Flow_0zp5mss" sourceRef="Activity_PreData" targetRef="Activity_SelectBoolean" />
<bpmn:scriptTask id="Activity_PreData" name="Pre Data">
<bpmn:incoming>Flow_1i32jb7</bpmn:incoming>
@ -43,17 +36,24 @@
<bpmn:script>if not 'yes_no' in globals():
yes_no = False</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1i32jb7" sourceRef="Set_Default" targetRef="Activity_PreData" />
<bpmn:userTask id="Set_Default" name="Hello" camunda:formKey="set_default">
<bpmn:documentation>&lt;H1&gt;Hello&lt;/H1&gt;</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="yes_no" label="Set A default" type="boolean">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1x41riu</bpmn:incoming>
<bpmn:outgoing>Flow_1i32jb7</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_TestBooleanDefault">
<bpmndi:BPMNEdge id="Flow_0f3gndz_di" bpmnElement="Flow_0f3gndz">
<di:waypoint x="820" y="117" />
<di:waypoint x="882" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0m31ypa_di" bpmnElement="Flow_0m31ypa">
<di:waypoint x="662" y="117" />
<di:waypoint x="720" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0zp5mss_di" bpmnElement="Flow_0zp5mss">
<di:waypoint x="500" y="117" />
<di:waypoint x="562" y="117" />
@ -66,6 +66,17 @@
<di:waypoint x="188" y="117" />
<di:waypoint x="240" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0f3gndz_di" bpmnElement="Flow_0f3gndz">
<di:waypoint x="820" y="117" />
<di:waypoint x="882" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0m31ypa_di" bpmnElement="Flow_0m31ypa">
<di:waypoint x="662" y="117" />
<di:waypoint x="720" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0vujv1w_di" bpmnElement="Activity_SelectBoolean">
<dc:Bounds x="562" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
@ -75,15 +86,12 @@
<bpmndi:BPMNShape id="Event_0rgpb6o_di" bpmnElement="Event_0rgpb6o">
<dc:Bounds x="882" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0vu3ozg_di" bpmnElement="Activity_Hello">
<dc:Bounds x="240" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ug5gxt_di" bpmnElement="Activity_PreData">
<dc:Bounds x="400" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0hks5xj_di" bpmnElement="Set_Default">
<dc:Bounds x="240" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -27,7 +27,7 @@ email(subject=subject,recipients=recipients)</bpmn:script>
<bpmn:sequenceFlow id="Flow_1synsig" sourceRef="StartEvent_1" targetRef="Activity_1l9vih3" />
<bpmn:sequenceFlow id="Flow_1xlrgne" sourceRef="Activity_0s5v97n" targetRef="Event_0izrcj4" />
<bpmn:sequenceFlow id="Flow_08n2npe" sourceRef="Activity_1l9vih3" targetRef="Activity_0s5v97n" />
<bpmn:userTask id="Activity_1l9vih3" name="Set Recipients">
<bpmn:userTask id="Activity_1l9vih3" name="Set Recipients" camunda:formKey="MyFormKey">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="ApprvlApprvr1" label="Approver" type="string" />

View File

@ -1,5 +1,5 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="MultiInstance" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" name="StartEvent_1">
<bpmn:outgoing>Flow_0t6p1sb</bpmn:outgoing>
@ -15,7 +15,7 @@
## Role: {{investigator.type_full}}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="email" label="Email Address:" type="string" />
<camunda:formField id="investigator.email" label="Email Address:" type="string" />
</camunda:formData>
<camunda:properties>
<camunda:property name="display_name" value="investigator.label" />

View File

@ -1,17 +1,12 @@
<?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:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0l37fag" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<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:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0l37fag" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_TestValueExpression" name="Test Value Expression" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1nc3qi5</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1nc3qi5" sourceRef="StartEvent_1" targetRef="Activity_Hello" />
<bpmn:sequenceFlow id="Flow_1t2lo17" sourceRef="Activity_Hello" targetRef="Activity_PreData" />
<bpmn:sequenceFlow id="Flow_1nc3qi5" sourceRef="StartEvent_1" targetRef="Activity_Set_Expression" />
<bpmn:sequenceFlow id="Flow_1t2lo17" sourceRef="Activity_Set_Expression" targetRef="Activity_PreData" />
<bpmn:sequenceFlow id="Flow_1hhfj67" sourceRef="Activity_PreData" targetRef="Activity_Data" />
<bpmn:manualTask id="Activity_Hello" name="Hello">
<bpmn:documentation>&lt;H1&gt;Hello&lt;/H1&gt;</bpmn:documentation>
<bpmn:incoming>Flow_1nc3qi5</bpmn:incoming>
<bpmn:outgoing>Flow_1t2lo17</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:scriptTask id="Activity_PreData" name="Pre Data">
<bpmn:incoming>Flow_1t2lo17</bpmn:incoming>
<bpmn:outgoing>Flow_1hhfj67</bpmn:outgoing>
@ -43,34 +38,48 @@
<bpmn:incoming>Flow_057as2q</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_057as2q" sourceRef="Activity_GoodBye" targetRef="Event_06wbkzi" />
<bpmn:userTask id="Activity_Set_Expression" name="Hello" camunda:formKey="value_expression_form">
<bpmn:documentation>&lt;H1&gt;Hello&lt;/H1&gt;</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="value_expression_value" type="string">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1nc3qi5</bpmn:incoming>
<bpmn:outgoing>Flow_1t2lo17</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_TestValueExpression">
<bpmndi:BPMNEdge id="Flow_1nc3qi5_di" bpmnElement="Flow_1nc3qi5">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1t2lo17_di" bpmnElement="Flow_1t2lo17">
<di:waypoint x="370" y="117" />
<di:waypoint x="431" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hhfj67_di" bpmnElement="Flow_1hhfj67">
<di:waypoint x="531" y="117" />
<di:waypoint x="590" y="117" />
<bpmndi:BPMNEdge id="Flow_057as2q_di" bpmnElement="Flow_057as2q">
<di:waypoint x="850" y="117" />
<di:waypoint x="912" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1skkg5a_di" bpmnElement="Flow_1skkg5a">
<di:waypoint x="690" y="117" />
<di:waypoint x="750" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_057as2q_di" bpmnElement="Flow_057as2q">
<di:waypoint x="850" y="117" />
<di:waypoint x="912" y="117" />
<bpmndi:BPMNEdge id="Flow_1hhfj67_di" bpmnElement="Flow_1hhfj67">
<di:waypoint x="531" y="117" />
<di:waypoint x="590" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1t2lo17_di" bpmnElement="Flow_1t2lo17">
<di:waypoint x="370" y="117" />
<di:waypoint x="431" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1nc3qi5_di" bpmnElement="Flow_1nc3qi5">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0hi68vh_di" bpmnElement="Activity_Hello">
<dc:Bounds x="270" y="77" width="100" height="80" />
<bpmndi:BPMNShape id="Activity_115dslj_di" bpmnElement="Activity_PreData">
<dc:Bounds x="431" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1i60o9l_di" bpmnElement="Activity_Data">
<dc:Bounds x="590" y="77" width="100" height="80" />
@ -81,8 +90,8 @@
<bpmndi:BPMNShape id="Event_06wbkzi_di" bpmnElement="Event_06wbkzi">
<dc:Bounds x="912" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_115dslj_di" bpmnElement="Activity_PreData">
<dc:Bounds x="431" y="77" width="100" height="80" />
<bpmndi:BPMNShape id="Activity_1wwwyog_di" bpmnElement="Activity_Set_Expression">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -1,13 +1,7 @@
from unittest.mock import patch
from crc import session
from crc.models.api_models import MultiInstanceType
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowStatus
from tests.base_test import BaseTest
from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
class TestWorkflowProcessorLoopingTask(BaseTest):

View File

@ -0,0 +1,111 @@
import json
import random
from unittest.mock import patch
from tests.base_test import BaseTest
from crc import session, app
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType
from crc.models.workflow import WorkflowStatus
from example_data import ExampleDataLoader
class TestMultiinstanceTasksApi(BaseTest):
@patch('crc.services.protocol_builder.requests.get')
def test_multi_instance_task(self, mock_get):
ExampleDataLoader().load_reference_documents()
# Enable the protocol builder.
app.config['PB_ENABLED'] = True
# This depends on getting a list of investigators back from the protocol builder.
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
workflow = self.create_workflow('multi_instance')
# get the first form in the two form workflow.
workflow = self.get_workflow_api(workflow)
navigation = self.get_workflow_api(workflow).navigation
self.assertEqual(5, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual("UserTask", workflow.next_task.type)
self.assertEqual(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
self.assertEqual(5, workflow.next_task.multi_instance_count)
# Assure that the names for each task are properly updated, so they aren't all the same.
self.assertEqual("Primary Investigator", workflow.next_task.title)
@patch('crc.services.protocol_builder.requests.get')
def test_parallel_multi_instance(self, mock_get):
# Assure we get nine investigators back from the API Call, as set in the investigators.json file.
app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
ExampleDataLoader().load_reference_documents()
workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow)
self.assertEqual(9, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav.state == "READY"]
self.assertEqual(5, len(ready_items))
self.assertEqual("UserTask", workflow_api.next_task.type)
self.assertEqual("MultiInstanceTask",workflow_api.next_task.name)
self.assertEqual("Primary Investigator", workflow_api.next_task.title)
for i in random.sample(range(5), 5):
task_id = ready_items[i].task_id
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, task_id),
headers=self.logged_in_headers(),
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
data['investigator']['email'] = "dhf8r@virginia.edu"
self.complete_form(workflow, workflow.next_task, data)
#tasks = self.get_workflow_api(workflow).user_tasks
workflow = self.get_workflow_api(workflow)
self.assertEqual(WorkflowStatus.complete, workflow.status)
@patch('crc.services.protocol_builder.requests.get')
def test_parallel_multi_instance_update_all(self, mock_get):
# Assure we get nine investigators back from the API Call, as set in the investigators.json file.
app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
ExampleDataLoader().load_reference_documents()
workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow)
self.assertEqual(9, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav.state == "READY"]
self.assertEqual(5, len(ready_items))
self.assertEqual("UserTask", workflow_api.next_task.type)
self.assertEqual("MultiInstanceTask",workflow_api.next_task.name)
self.assertEqual("Primary Investigator", workflow_api.next_task.title)
data = workflow_api.next_task.data
data['investigator']['email'] = "dhf8r@virginia.edu"
self.complete_form(workflow, workflow_api.next_task, data, update_all=True)
workflow = self.get_workflow_api(workflow)
self.assertEqual(WorkflowStatus.complete, workflow.status)
data = workflow.next_task.data
for key in data["StudyInfo"]["investigators"]:
self.assertEquals("dhf8r@virginia.edu", data["StudyInfo"]["investigators"][key]['email'])

View File

@ -256,30 +256,6 @@ class TestTasksApi(BaseTest):
self.assertEqual("JustAValue", task.properties['JustAKey'])
@patch('crc.services.protocol_builder.requests.get')
def test_multi_instance_task(self, mock_get):
self.load_example_data()
# Enable the protocol builder.
app.config['PB_ENABLED'] = True
# This depends on getting a list of investigators back from the protocol builder.
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
workflow = self.create_workflow('multi_instance')
# get the first form in the two form workflow.
workflow = self.get_workflow_api(workflow)
navigation = self.get_workflow_api(workflow).navigation
self.assertEqual(5, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual("UserTask", workflow.next_task.type)
self.assertEqual(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
self.assertEqual(5, workflow.next_task.multi_instance_count)
# Assure that the names for each task are properly updated, so they aren't all the same.
self.assertEqual("Primary Investigator", workflow.next_task.title)
def test_lookup_endpoint_for_task_field_enumerations(self):
workflow = self.create_workflow('enum_options_with_search')
@ -445,42 +421,3 @@ class TestTasksApi(BaseTest):
workflow = self.get_workflow_api(workflow)
self.assertEqual('Task_Why_No_Bananas', workflow.next_task.name)
@patch('crc.services.protocol_builder.requests.get')
def test_parallel_multi_instance(self, mock_get):
# Assure we get nine investigators back from the API Call, as set in the investigators.json file.
app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
self.load_example_data()
workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow)
self.assertEqual(9, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav.state == "READY"]
self.assertEqual(5, len(ready_items))
self.assertEqual("UserTask", workflow_api.next_task.type)
self.assertEqual("MultiInstanceTask",workflow_api.next_task.name)
self.assertEqual("Primary Investigator", workflow_api.next_task.title)
for i in random.sample(range(5), 5):
task_id = ready_items[i].task_id
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, task_id),
headers=self.logged_in_headers(),
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
data['investigator']['email'] = "dhf8r@virginia.edu"
self.complete_form(workflow, workflow.next_task, data)
#tasks = self.get_workflow_api(workflow).user_tasks
workflow = self.get_workflow_api(workflow)
self.assertEqual(WorkflowStatus.complete, workflow.status)

View File

@ -6,8 +6,8 @@ class TestBooleanDefault(BaseTest):
def do_test(self, yes_no):
workflow = self.create_workflow('boolean_default_value')
workflow_api = self.get_workflow_api(workflow)
first_task = workflow_api.next_task
result = self.complete_form(workflow_api, first_task, {'yes_no': yes_no})
set_default_task = workflow_api.next_task
result = self.complete_form(workflow_api, set_default_task, {'yes_no': yes_no})
return result
def test_boolean_true_string(self):

View File

@ -98,3 +98,19 @@ class TestWorkflowService(BaseTest):
def test_expressions_in_forms(self):
workflow_spec_model = self.load_test_spec("form_expressions")
WorkflowService.test_spec(workflow_spec_model.id)
def test_set_value(self):
destiation = {}
path = "a.b.c"
value = "abracadara"
result = WorkflowService.set_dot_value(path, value, destiation)
self.assertEqual(value, destiation["a"]["b"]["c"])
def test_get_dot_value(self):
path = "a.b.c"
source = {"a":{"b":{"c" : "abracadara"}}, "a.b.c":"garbage"}
result = WorkflowService.get_dot_value(path, source)
self.assertEqual("abracadara", result)
result2 = WorkflowService.get_dot_value(path, {"a.b.c":"garbage"})
self.assertEqual("garbage", result2)