mirror of
https://github.com/sartography/cr-connect-workflow.git
synced 2025-02-22 12:48:25 +00:00
Merge branch 'dev' into 263-optimize-dashboard
# Conflicts: # crc/services/study_service.py
This commit is contained in:
commit
116bf5e7aa
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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': '',
|
||||||
|
@ -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}}}
|
||||||
|
|
||||||
|
@ -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). \
|
||||||
|
@ -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)
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
@ -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><div><span>Good Bye {{ dog.name }}</span></div>
|
||||||
|
<div><span>You are such a good {{ dog.breed }}</span></div>
|
||||||
|
</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>
|
@ -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>
|
97
tests/data/infinite_loop/infinite_loop.bpmn
Normal file
97
tests/data/infinite_loop/infinite_loop.bpmn
Normal 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>
|
66
tests/study/test_study_status_message.py
Normal file
66
tests/study/test_study_status_message.py
Normal 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)
|
15
tests/test_decision_table_dictionary_output.py
Normal file
15
tests/test_decision_table_dictionary_output.py
Normal 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'])
|
13
tests/workflow/test_workflow_infinite_loop.py
Normal file
13
tests/workflow/test_workflow_infinite_loop.py
Normal 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'])
|
Loading…
x
Reference in New Issue
Block a user