Merge branch 'dev' into dependabot/pip/lxml-4.6.3

This commit is contained in:
Dan 2021-04-02 14:55:59 -04:00
commit 33a87f6c35
7 changed files with 252 additions and 18 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

@ -71,7 +71,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)
@ -83,8 +83,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:
@ -416,24 +417,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). \
@ -446,7 +459,7 @@ class StudyService(object):
return workflow_metas return workflow_metas
@staticmethod @staticmethod
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

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