Merge branch 'dev' into 263-optimize-dashboard

# Conflicts:
#	crc/services/study_service.py
This commit is contained in:
Kelly McDonald 2021-03-31 10:20:33 -04:00
commit 116bf5e7aa
14 changed files with 410 additions and 51 deletions

View File

@ -160,7 +160,6 @@ paths:
parameters: parameters:
- name: workflow_spec_id - name: workflow_spec_id
in: path in: path
required: false
description: The unique id of an existing workflow specification to modify. description: The unique id of an existing workflow specification to modify.
schema: schema:
type: string type: string

View File

@ -96,7 +96,7 @@ class WorkflowMetadata(object):
def __init__(self, id, name = None, display_name = None, description = None, spec_version = None, def __init__(self, id, name = None, display_name = None, description = None, spec_version = None,
category_id = None, category_display_name = None, state: WorkflowState = None, category_id = None, category_display_name = None, state: WorkflowState = None,
status: WorkflowStatus = None, total_tasks = None, completed_tasks = None, status: WorkflowStatus = None, total_tasks = None, completed_tasks = None,
is_review=None,display_order = None): is_review=None,display_order = None, state_message = None):
self.id = id self.id = id
self.name = name self.name = name
self.display_name = display_name self.display_name = display_name
@ -105,6 +105,7 @@ class WorkflowMetadata(object):
self.category_id = category_id self.category_id = category_id
self.category_display_name = category_display_name self.category_display_name = category_display_name
self.state = state self.state = state
self.state_message = state_message
self.status = status self.status = status
self.total_tasks = total_tasks self.total_tasks = total_tasks
self.completed_tasks = completed_tasks self.completed_tasks = completed_tasks
@ -140,7 +141,7 @@ class WorkflowMetadataSchema(ma.Schema):
model = WorkflowMetadata model = WorkflowMetadata
additional = ["id", "name", "display_name", "description", additional = ["id", "name", "display_name", "description",
"total_tasks", "completed_tasks", "display_order", "total_tasks", "completed_tasks", "display_order",
"category_id", "is_review", "category_display_name"] "category_id", "is_review", "category_display_name", "state_message"]
unknown = INCLUDE unknown = INCLUDE

View File

@ -80,7 +80,12 @@ class StudyInfo(Script):
'display': 'Optional', 'display': 'Optional',
'unique': 'Yes', 'unique': 'Yes',
'user_id': 'asd3v', 'user_id': 'asd3v',
'error': 'Unable to locate a user with id asd3v in LDAP'} 'error': 'Unable to locate a user with id asd3v in LDAP'},
'DEPT_CH': {
'label': 'Department Chair',
'display': 'Always',
'unique': 'Yes',
'user_id': 'lb3dp'}
}, },
"documents": { "documents": {
'AD_CoCApp': {'category1': 'Ancillary Document', 'category2': 'CoC Application', 'category3': '', 'AD_CoCApp': {'category1': 'Ancillary Document', 'category2': 'CoC Application', 'category3': '',

View File

@ -17,7 +17,8 @@ generic_message = """Workflow validation failed. For more information about the
known_errors = {'Error is Non-default exclusive outgoing sequence flow without condition': known_errors = {'Error is Non-default exclusive outgoing sequence flow without condition':
{'hint': 'Add a Condition Type to your gateway path.'}, {'hint': 'Add a Condition Type to your gateway path.'},
'Could not set task title on task (\w+) with \'(.*)\' property because \\1: Error evaluating expression \'(.*)\', "\'Box\' object has no attribute \'\\2\'"$': 'Could not set task title on task (\w+) with \'(.*)\' property because \\1: Error evaluating '
'expression \'(.*)\', $':
{'hint': 'You are overriding the title for task `{task_id}`, using the `{property}` extension, and it is causing an error. Look under the extensions tab for the task, and check the value you are setting for the property.', {'hint': 'You are overriding the title for task `{task_id}`, using the `{property}` extension, and it is causing an error. Look under the extensions tab for the task, and check the value you are setting for the property.',
'groups': {'task_id': 0, 'property': 1}}} 'groups': {'task_id': 0, 'property': 1}}}

View File

@ -1,7 +1,7 @@
from copy import copy from copy import copy
from datetime import datetime from datetime import datetime
from typing import List from typing import List
from crc.services.cache_service import timeit
import requests import requests
from SpiffWorkflow import WorkflowException from SpiffWorkflow import WorkflowException
from SpiffWorkflow.exceptions import WorkflowTaskExecException from SpiffWorkflow.exceptions import WorkflowTaskExecException
@ -53,7 +53,6 @@ class StudyService(object):
return studies return studies
@staticmethod @staticmethod
@timeit
def get_study(study_id, study_model: StudyModel = None, do_status=True): def get_study(study_id, study_model: StudyModel = None, do_status=True):
"""Returns a study model that contains all the workflows organized by category. """Returns a study model that contains all the workflows organized by category.
IMPORTANT: This is intended to be a lightweight call, it should never involve IMPORTANT: This is intended to be a lightweight call, it should never involve
@ -73,7 +72,7 @@ class StudyService(object):
study.last_activity_user = LdapService.user_info(last_event.user_uid).display_name study.last_activity_user = LdapService.user_info(last_event.user_uid).display_name
study.last_activity_date = last_event.date study.last_activity_date = last_event.date
study.categories = StudyService.get_categories() study.categories = StudyService.get_categories()
workflow_metas = StudyService.__get_workflow_metas(study_id) workflow_metas = StudyService._get_workflow_metas(study_id)
files = FileService.get_files_for_study(study.id) files = FileService.get_files_for_study(study.id)
files = (File.from_models(model, FileService.get_file_data(model.id), files = (File.from_models(model, FileService.get_file_data(model.id),
FileService.get_doc_dictionary()) for model in files) FileService.get_doc_dictionary()) for model in files)
@ -85,8 +84,9 @@ class StudyService(object):
# this line is taking 99% of the time that is used in get_study. # this line is taking 99% of the time that is used in get_study.
# see ticket #196 # see ticket #196
if do_status: if do_status:
status = StudyService.__get_study_status(study_model) # __get_study_status() runs the master workflow to generate the status dictionary
study.warnings = StudyService.__update_status_of_workflow_meta(workflow_metas, status) status = StudyService._get_study_status(study_model)
study.warnings = StudyService._update_status_of_workflow_meta(workflow_metas, status)
# Group the workflows into their categories. # Group the workflows into their categories.
for category in study.categories: for category in study.categories:
@ -418,24 +418,36 @@ class StudyService(object):
db.session.commit() db.session.commit()
@staticmethod @staticmethod
def __update_status_of_workflow_meta(workflow_metas, status): def _update_status_of_workflow_meta(workflow_metas, status):
# Update the status on each workflow # Update the status on each workflow
warnings = [] warnings = []
for wfm in workflow_metas: for wfm in workflow_metas:
if wfm.name in status.keys(): wfm.state_message = ''
if not WorkflowState.has_value(status[wfm.name]): # do we have a status for you
warnings.append(ApiError("invalid_status", if wfm.name not in status.keys():
"Workflow '%s' can not be set to '%s', should be one of %s" % (
wfm.name, status[wfm.name], ",".join(WorkflowState.list())
)))
else:
wfm.state = WorkflowState[status[wfm.name]]
else:
warnings.append(ApiError("missing_status", "No status specified for workflow %s" % wfm.name)) warnings.append(ApiError("missing_status", "No status specified for workflow %s" % wfm.name))
continue
if not isinstance(status[wfm.name], dict):
warnings.append(ApiError(code='invalid_status',
message=f'Status must be a dictionary with "status" and "message" keys. Name is {wfm.name}. Status is {status[wfm.name]}'))
continue
if 'status' not in status[wfm.name].keys():
warnings.append(ApiError("missing_status",
"Workflow '%s' does not have a status setting" % wfm.name))
continue
if not WorkflowState.has_value(status[wfm.name]['status']):
warnings.append(ApiError("invalid_state",
"Workflow '%s' can not be set to '%s', should be one of %s" % (
wfm.name, status[wfm.name]['status'], ",".join(WorkflowState.list())
)))
continue
wfm.state = WorkflowState[status[wfm.name]['status']]
if 'message' in status[wfm.name].keys():
wfm.state_message = status[wfm.name]['message']
return warnings return warnings
@staticmethod @staticmethod
def __get_workflow_metas(study_id): def _get_workflow_metas(study_id):
# Add in the Workflows for each category # Add in the Workflows for each category
workflow_models = db.session.query(WorkflowModel). \ workflow_models = db.session.query(WorkflowModel). \
join(WorkflowSpecModel). \ join(WorkflowSpecModel). \
@ -448,8 +460,7 @@ class StudyService(object):
return workflow_metas return workflow_metas
@staticmethod @staticmethod
@timeit def _get_study_status(study_model):
def __get_study_status(study_model):
"""Uses the Top Level Workflow to calculate the status of the study, and it's """Uses the Top Level Workflow to calculate the status of the study, and it's
workflow models.""" workflow models."""
master_specs = db.session.query(WorkflowSpecModel). \ master_specs = db.session.query(WorkflowSpecModel). \

View File

@ -95,31 +95,38 @@ class WorkflowService(object):
WorkflowService.delete_test_data() WorkflowService.delete_test_data()
raise ApiError.from_workflow_exception("workflow_validation_exception", str(we), we) raise ApiError.from_workflow_exception("workflow_validation_exception", str(we), we)
count = 0
while not processor.bpmn_workflow.is_completed(): while not processor.bpmn_workflow.is_completed():
try: if count < 100: # check for infinite loop
processor.bpmn_workflow.get_deep_nav_list() # Assure no errors with navigation. try:
processor.bpmn_workflow.do_engine_steps() processor.bpmn_workflow.get_deep_nav_list() # Assure no errors with navigation.
tasks = processor.bpmn_workflow.get_tasks(SpiffTask.READY) processor.bpmn_workflow.do_engine_steps()
for task in tasks: tasks = processor.bpmn_workflow.get_tasks(SpiffTask.READY)
if task.task_spec.lane is not None and task.task_spec.lane not in task.data: for task in tasks:
raise ApiError.from_task("invalid_role", if task.task_spec.lane is not None and task.task_spec.lane not in task.data:
f"This task is in a lane called '{task.task_spec.lane}', The " raise ApiError.from_task("invalid_role",
f" current task data must have information mapping this role to " f"This task is in a lane called '{task.task_spec.lane}', The "
f" a unique user id.", task) f" current task data must have information mapping this role to "
task_api = WorkflowService.spiff_task_to_api_task( f" a unique user id.", task)
task, task_api = WorkflowService.spiff_task_to_api_task(
add_docs_and_forms=True) # Assure we try to process the documentation, and raise those errors. task,
# make sure forms have a form key add_docs_and_forms=True) # Assure we try to process the documentation, and raise those errors.
if hasattr(task_api, 'form') and task_api.form is not None and task_api.form.key == '': # make sure forms have a form key
raise ApiError(code='missing_form_key', if hasattr(task_api, 'form') and task_api.form is not None and task_api.form.key == '':
message='Forms must include a Form Key.', raise ApiError(code='missing_form_key',
task_id=task.id, message='Forms must include a Form Key.',
task_name=task.get_name()) task_id=task.id,
WorkflowService.populate_form_with_random_data(task, task_api, required_only) task_name=task.get_name())
processor.complete_task(task) WorkflowService.populate_form_with_random_data(task, task_api, required_only)
except WorkflowException as we: processor.complete_task(task)
WorkflowService.delete_test_data() count += 1
raise ApiError.from_workflow_exception("workflow_validation_exception", str(we), we) except WorkflowException as we:
WorkflowService.delete_test_data()
raise ApiError.from_workflow_exception("workflow_validation_exception", str(we), we)
else:
raise ApiError.from_task(code='validation_loop',
message=f'There appears to be an infinite loop in the validation. Task is {task.task_spec.description}',
task=task)
WorkflowService.delete_test_data() WorkflowService.delete_test_data()
WorkflowService._process_documentation(processor.bpmn_workflow.last_task.parent.parent) WorkflowService._process_documentation(processor.bpmn_workflow.last_task.parent.parent)

View File

@ -89,6 +89,10 @@
padding-top: 10px; padding-top: 10px;
} }
td#logo-td {
width: 50px;
}
.footer, .header { .footer, .header {
clear: both; clear: both;
margin-top: 10px; margin-top: 10px;
@ -361,7 +365,7 @@
<table role="presentation"> <table role="presentation">
<tr> <tr>
<th role="presentation"></th> <th role="presentation"></th>
<td> <td id="logo-td">
<img class="logo" <img class="logo"
src="{{ url_for('static', filename='uva_rotunda.svg', _external=True) }}" src="{{ url_for('static', filename='uva_rotunda.svg', _external=True) }}"
alt="University of Virginia"> alt="University of Virginia">

View File

@ -34,10 +34,10 @@ imagesize==1.2.0
inflection==0.5.1 inflection==0.5.1
itsdangerous==1.1.0 itsdangerous==1.1.0
jdcal==1.4.1 jdcal==1.4.1
jinja2==2.11.2 jinja2==2.11.3
jsonschema==3.2.0 jsonschema==3.2.0
ldap3==2.8.1 ldap3==2.8.1
lxml==4.6.2 lxml==4.6.3
mako==1.1.3 mako==1.1.3
markdown==3.3.3 markdown==3.3.3
markupsafe==1.1.1 markupsafe==1.1.1
@ -63,7 +63,7 @@ python-docx==0.8.10
python-editor==1.0.4 python-editor==1.0.4
python-levenshtein==0.12.0 python-levenshtein==0.12.0
pytz==2020.4 pytz==2020.4
pyyaml==5.3.1 pyyaml==5.4
recommonmark==0.6.0 recommonmark==0.6.0
requests==2.25.0 requests==2.25.0
sentry-sdk==0.14.4 sentry-sdk==0.14.4

View File

@ -0,0 +1,71 @@
<?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_09rv9vf" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_StatusMessage" name="Status Message" isExecutable="true">
<bpmn:documentation>Testing Workflow Status Messages</bpmn:documentation>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0x4n744</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0x4n744" sourceRef="StartEvent_1" targetRef="Task_SetName" />
<bpmn:sequenceFlow id="SequenceFlow_1o630oy" sourceRef="Task_SetName" targetRef="Task_Decision" />
<bpmn:businessRuleTask id="Task_Decision" name="Make Decision" camunda:decisionRef="Decision_Dog">
<bpmn:incoming>SequenceFlow_1o630oy</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1foyag7</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:sequenceFlow id="SequenceFlow_1foyag7" sourceRef="Task_Decision" targetRef="Task_GoodBye" />
<bpmn:manualTask id="Task_GoodBye" name="Say Good Bye">
<bpmn:documentation>&lt;div&gt;&lt;span&gt;Good Bye {{ dog.name }}&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;You are such a good {{ dog.breed }}&lt;/span&gt;&lt;/div&gt;
</bpmn:documentation>
<bpmn:incoming>SequenceFlow_1foyag7</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1bc1ugw</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="EndEvent_19dasnt">
<bpmn:incoming>SequenceFlow_1bc1ugw</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_1bc1ugw" sourceRef="Task_GoodBye" targetRef="EndEvent_19dasnt" />
<bpmn:userTask id="Task_SetName" name="Set Name" camunda:formKey="NameForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="name" label="Name" type="string" defaultValue="Layla" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0x4n744</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1o630oy</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_StatusMessage">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="165" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0x4n744_di" bpmnElement="SequenceFlow_0x4n744">
<di:waypoint x="201" y="117" />
<di:waypoint x="260" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1o630oy_di" bpmnElement="SequenceFlow_1o630oy">
<di:waypoint x="360" y="117" />
<di:waypoint x="420" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="BusinessRuleTask_0dwwkqn_di" bpmnElement="Task_Decision">
<dc:Bounds x="420" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1foyag7_di" bpmnElement="SequenceFlow_1foyag7">
<di:waypoint x="520" y="117" />
<di:waypoint x="580" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ManualTask_0nb6k7f_di" bpmnElement="Task_GoodBye">
<dc:Bounds x="580" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_19dasnt_di" bpmnElement="EndEvent_19dasnt">
<dc:Bounds x="742" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1bc1ugw_di" bpmnElement="SequenceFlow_1bc1ugw">
<di:waypoint x="680" y="117" />
<di:waypoint x="742" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_1h3sio1_di" bpmnElement="Task_SetName">
<dc:Bounds x="260" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_1eg3sxk" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<decision id="Decision_Dog" name="Dogs">
<decisionTable id="decisionTable_1">
<input id="input_1" label="Name">
<inputExpression id="inputExpression_1" typeRef="string">
<text>name</text>
</inputExpression>
</input>
<output id="output_1" label="Name" name="dog.name" typeRef="string" />
<output id="OutputClause_1wqk5xi" label="Breed" name="dog.breed" typeRef="string" />
<rule id="DecisionRule_1h6w5qu">
<inputEntry id="UnaryTests_0m8eblt">
<text>'Layla'</text>
</inputEntry>
<outputEntry id="LiteralExpression_186aovp">
<text>'Layla'</text>
</outputEntry>
<outputEntry id="LiteralExpression_10jbx5v">
<text>'Aussie'</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0ziemrx">
<inputEntry id="UnaryTests_0okkroj">
<text>'Mona'</text>
</inputEntry>
<outputEntry id="LiteralExpression_0r1apkh">
<text>'Mona'</text>
</outputEntry>
<outputEntry id="LiteralExpression_08vl869">
<text>'Aussie Mix'</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0fykwob">
<inputEntry id="UnaryTests_044ophk">
<text>'Jerry'</text>
</inputEntry>
<outputEntry id="LiteralExpression_0508umo">
<text>'Jerry'</text>
</outputEntry>
<outputEntry id="LiteralExpression_0ysgqib">
<text>'Aussie Mix'</text>
</outputEntry>
</rule>
<rule id="DecisionRule_05jugdn">
<inputEntry id="UnaryTests_1jri40s">
<text>'Zoey'</text>
</inputEntry>
<outputEntry id="LiteralExpression_1r5jrzq">
<text>'Zoey'</text>
</outputEntry>
<outputEntry id="LiteralExpression_0aqjmjy">
<text>'Healer'</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0gehtk4">
<inputEntry id="UnaryTests_09f1t9t">
<text>'Etta'</text>
</inputEntry>
<outputEntry id="LiteralExpression_0kp8mvr">
<text>'Etta'</text>
</outputEntry>
<outputEntry id="LiteralExpression_0wwry9c">
<text>'Healer Mix'</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -0,0 +1,97 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_81799d0" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_InfiniteLoop" name="Infinite Loop" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0ldlhrt</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0ldlhrt" sourceRef="StartEvent_1" targetRef="Activity_StudyInfo" />
<bpmn:scriptTask id="Activity_StudyInfo" name="Get Study Info">
<bpmn:incoming>Flow_0ldlhrt</bpmn:incoming>
<bpmn:incoming>Flow_05mrx8v</bpmn:incoming>
<bpmn:outgoing>Flow_0pddur1</bpmn:outgoing>
<bpmn:script>investigators = study_info('investigators')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0pddur1" sourceRef="Activity_StudyInfo" targetRef="Activity_DisplayInvestigators" />
<bpmn:manualTask id="Activity_DisplayInvestigators" name="Display Investigators">
<bpmn:documentation>Investigators: {{ investigators }}</bpmn:documentation>
<bpmn:incoming>Flow_0pddur1</bpmn:incoming>
<bpmn:outgoing>Flow_03m3cuy</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:exclusiveGateway id="Gateway_0n9lzir" name="Test DEPT_CH" default="Flow_05mrx8v">
<bpmn:incoming>Flow_03m3cuy</bpmn:incoming>
<bpmn:outgoing>Flow_05mrx8v</bpmn:outgoing>
<bpmn:outgoing>Flow_1212fe2</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_03m3cuy" sourceRef="Activity_DisplayInvestigators" targetRef="Gateway_0n9lzir" />
<bpmn:sequenceFlow id="Flow_05mrx8v" name="not has DEPT_CH" sourceRef="Gateway_0n9lzir" targetRef="Activity_StudyInfo" />
<bpmn:sequenceFlow id="Flow_1212fe2" name="Has DEPT_CH" sourceRef="Gateway_0n9lzir" targetRef="Activity_GoodBye">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">hasattr(investigators, 'DEPT_CH')</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:endEvent id="Event_0azm9il">
<bpmn:incoming>Flow_14jn215</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_14jn215" sourceRef="Activity_GoodBye" targetRef="Event_0azm9il" />
<bpmn:manualTask id="Activity_GoodBye" name="Good Bye">
<bpmn:documentation># Thank You</bpmn:documentation>
<bpmn:incoming>Flow_1212fe2</bpmn:incoming>
<bpmn:outgoing>Flow_14jn215</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_InfiniteLoop">
<bpmndi:BPMNEdge id="Flow_14jn215_di" bpmnElement="Flow_14jn215">
<di:waypoint x="810" y="177" />
<di:waypoint x="882" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1212fe2_di" bpmnElement="Flow_1212fe2">
<di:waypoint x="645" y="177" />
<di:waypoint x="710" y="177" />
<bpmndi:BPMNLabel>
<dc:Bounds x="645" y="159" width="74" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_05mrx8v_di" bpmnElement="Flow_05mrx8v">
<di:waypoint x="620" y="152" />
<di:waypoint x="620" y="80" />
<di:waypoint x="320" y="80" />
<di:waypoint x="320" y="137" />
<bpmndi:BPMNLabel>
<dc:Bounds x="447" y="62" width="52" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_03m3cuy_di" bpmnElement="Flow_03m3cuy">
<di:waypoint x="530" y="177" />
<di:waypoint x="595" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0pddur1_di" bpmnElement="Flow_0pddur1">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ldlhrt_di" bpmnElement="Flow_0ldlhrt">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1ueb1ky_di" bpmnElement="Activity_StudyInfo">
<dc:Bounds x="270" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1y5mgz2_di" bpmnElement="Activity_DisplayInvestigators">
<dc:Bounds x="430" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0n9lzir_di" bpmnElement="Gateway_0n9lzir" isMarkerVisible="true">
<dc:Bounds x="595" y="152" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="584" y="209" width="75" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0azm9il_di" bpmnElement="Event_0azm9il">
<dc:Bounds x="882" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0wbzf51_di" bpmnElement="Activity_GoodBye">
<dc:Bounds x="710" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,66 @@
from tests.base_test import BaseTest
from crc import db, session
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowState
from crc.services.study_service import StudyService
class TestStudyStatusMessage(BaseTest):
"""The workflow runs with a workflow_meta.name of `random_fact`
Add an entry to `status` dictionary with a key of `random_fact`"""
def run_update_status(self, status):
# shared code
self.load_example_data()
study_model = session.query(StudyModel).first()
workflow_metas = StudyService._get_workflow_metas(study_model.id)
warnings = StudyService._update_status_of_workflow_meta(workflow_metas, status)
return workflow_metas, warnings
def test_study_status_message(self):
# these are the passing tests
# we loop through each Workflow state
# (hidden,disabled,required,optional)
for state in WorkflowState:
# use state.value to set status['status'],
status = {'random_fact':
{'status': state.value,
'message': 'This is my status message!'}}
# call run_update_status(),
workflow_metas, warnings = self.run_update_status(status)
# and assert the values of workflow_metas[0].state and workflow_metas[0].state_message
self.assertEqual(0, len(warnings))
self.assertEqual(state, workflow_metas[0].state)
self.assertEqual('This is my status message!', workflow_metas[0].state_message)
def test_study_status_message_bad_name(self):
# we don't have an entry for you in the status dictionary
status = {'bad_name': {'status': 'hidden', 'message': 'This is my status message!'}}
workflow_metas, warnings = self.run_update_status(status)
self.assertEqual(1, len(warnings))
self.assertEqual('missing_status', warnings[0].code)
self.assertEqual('No status specified for workflow random_fact', warnings[0].message)
def test_study_status_message_not_dict(self):
# your entry in the status dictionary is not a dictionary
status = {'random_fact': 'This is my status message!'}
workflow_metas, warnings = self.run_update_status(status)
self.assertEqual(1, len(warnings))
self.assertEqual('invalid_status', warnings[0].code)
self.assertEqual('Status must be a dictionary with "status" and "message" keys. Name is random_fact. Status is This is my status message!',
warnings[0].message)
def test_study_status_message_bad_state(self):
# you have an invalid state
# I.e., not in (hidden,disabled,required,optional)
status = {'random_fact': {'status': 'hide', 'message': 'This is my status message!'}}
workflow_metas, warnings = self.run_update_status(status)
self.assertEqual(1, len(warnings))
self.assertEqual('invalid_state', warnings[0].code)
self.assertEqual('Workflow \'random_fact\' can not be set to \'hide\', should be one of hidden,disabled,required,optional',
warnings[0].message)

View File

@ -0,0 +1,15 @@
from tests.base_test import BaseTest
class TestDecisionTableDictionaryOutput(BaseTest):
def test_decision_table_dictionary_output(self):
workflow = self.create_workflow('decision_table_dictionary_output')
workflow_api = self.get_workflow_api(workflow)
first_task = workflow_api.next_task
result = self.complete_form(workflow, first_task, {'name': 'Mona'})
self.assertIn('dog', result.next_task.data)
self.assertEqual('Mona', result.next_task.data['dog']['name'])
self.assertEqual('Aussie Mix', result.next_task.data['dog']['breed'])

View File

@ -0,0 +1,13 @@
from tests.base_test import BaseTest
from crc import app
import json
class TestWorkflowInfiniteLoop(BaseTest):
def test_workflow_infinite_loop(self):
self.load_example_data()
spec_model = self.load_test_spec('infinite_loop')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
json_data = json.loads(rv.get_data(as_text=True))
self.assertIn('There appears to be an infinite loop', json_data[0]['message'])