Merge pull request #20 from sartography/feature/delete_study

Fixes #11: adding a delete endpoint for studies.  It won't delete stu…
This commit is contained in:
Aaron Louie 2020-03-16 10:17:02 -04:00 committed by GitHub
commit 091e422aea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 218 additions and 6 deletions

View File

@ -41,3 +41,18 @@ class StudyInfo(Script):
if cmd == 'details':
study_info["details"] = self.pb.get_study_details(study_id)
task.data["study"] = study_info
def get_required_docs(self, study_id):
required_docs = self.pb.get_required_docs(study_id)
return required_docs

View File

@ -4,6 +4,7 @@ import xml.etree.ElementTree as ElementTree
from SpiffWorkflow import Task as SpiffTask, Workflow
from SpiffWorkflow.bpmn.BpmnScriptEngine import BpmnScriptEngine
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from SpiffWorkflow.bpmn.serializer.BpmnSerializer import BpmnSerializer
from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
@ -98,6 +99,7 @@ class WorkflowProcessor(object):
completed task in the previous workflow.
If neither flag is set, it will use the same version of the specification that was used to originally
create the workflow model. """
orig_version = workflow_model.spec_version
if soft_reset:
spec = self.get_spec(workflow_model.workflow_spec_id)
workflow_model.spec_version = spec.description
@ -105,7 +107,17 @@ class WorkflowProcessor(object):
spec = self.get_spec(workflow_model.workflow_spec_id, workflow_model.spec_version)
self.workflow_spec_id = workflow_model.workflow_spec_id
self.bpmn_workflow = self._serializer.deserialize_workflow(workflow_model.bpmn_workflow_json, workflow_spec=spec)
try:
self.bpmn_workflow = self._serializer.deserialize_workflow(workflow_model.bpmn_workflow_json, workflow_spec=spec)
except KeyError as ke:
if soft_reset:
# Undo the soft-reset.
workflow_model.spec_version = orig_version
orig_version = workflow_model.spec_version
raise ApiError(code="unexpected_workflow_structure",
message="Failed to deserialize workflow '%s' version %s, due to a mis-placed or missing task '%s'" %
(self.workflow_spec_id, workflow_model.spec_version, str(ke)) +
" This is very likely due to a soft reset where there was a structural change.")
self.bpmn_workflow.script_engine = self._script_engine
if hard_reset:
@ -192,8 +204,14 @@ class WorkflowProcessor(object):
dmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
parser.add_dmn_xml(dmn, filename=file_data.file_model.name)
if process_id is None:
raise(Exception("There is no primary BPMN model defined for workflow %s" % workflow_spec_id))
spec = parser.get_spec(process_id)
raise(ApiError(code="no_primary_bpmn_error",
message="There is no primary BPMN model defined for workflow %s" % workflow_spec_id))
try:
spec = parser.get_spec(process_id)
except ValidationException as ve:
raise ApiError(code="workflow_validation_error",
message="Failed to parse Workflow Specification '%s' %s." % (workflow_spec_id, version) +
"Error is %s" % str(ve))
spec.description = version
return spec
@ -220,7 +238,7 @@ class WorkflowProcessor(object):
session.add(workflow_model)
session.commit()
# Need to commit twice, first to get a unique id for the workflow model, and
# a second time to store the serilaization so we can maintain this link within
# a second time to store the serialization so we can maintain this link within
# the spiff-workflow process.
bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = workflow_model.id
@ -320,7 +338,7 @@ class WorkflowProcessor(object):
process_elements.append(child)
if len(process_elements) == 0:
raise Exception('No executable process tag found')
raise ValidationException('No executable process tag found')
# There are multiple root elements
if len(process_elements) > 1:
@ -332,6 +350,6 @@ class WorkflowProcessor(object):
if child_element.tag.endswith('startEvent'):
return this_element.attrib['id']
raise Exception('No start event found in %s' % et_root.attrib['id'])
raise ValidationException('No start event found in %s' % et_root.attrib['id'])
return process_elements[0].attrib['id']

View File

@ -0,0 +1,116 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1j7idla" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_18biih5" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" />
<bpmn:userTask name="Has Bananas?" camunda:formKey="bananas_form">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="has_bananas" label="Do you have bananas?" type="boolean" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1pnq3kg</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1lmkn99</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1lmkn99" targetRef="ExclusiveGateway_003amsm" />
<bpmn:exclusiveGateway id="ExclusiveGateway_003amsm">
<bpmn:incoming>SequenceFlow_1lmkn99</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_No_Bananas</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_Yes_Bananas</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="SequenceFlow_No_Bananas" name="no" sourceRef="ExclusiveGateway_003amsm" targetRef="Task_Why_No_Bananas">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">lower_case_true==true</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:userTask id="Task_Num_Bananas" name="Number of Bananas" camunda:formKey="banana_count">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="num_bananas" label="How Many Bananas do you have?" type="long" defaultValue="1" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_Yes_Bananas</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_02z84p5</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_Why_No_Bananas" name="Why no bananas" camunda:formKey="no_bananas">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="why_no_bananas" label="Why you have no bananas?" type="string" defaultValue="I don&#39;t know." />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_No_Bananas</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_08djf6q</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="EndEvent_063bpg6">
<bpmn:incoming>SequenceFlow_02z84p5</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_02z84p5" sourceRef="Task_Num_Bananas" targetRef="EndEvent_063bpg6" />
<bpmn:endEvent id="EndEvent_1hwtug4">
<bpmn:incoming>SequenceFlow_08djf6q</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_08djf6q" sourceRef="Task_Why_No_Bananas" targetRef="EndEvent_1hwtug4" />
<bpmn:sequenceFlow id="SequenceFlow_1pnq3kg" sourceRef="StartEvent_1" />
<bpmn:sequenceFlow id="SequenceFlow_Yes_Bananas" name="yes&#10;&#10;" sourceRef="ExclusiveGateway_003amsm" targetRef="Task_Num_Bananas">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">has_bananas == True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:textAnnotation id="TextAnnotation_014touo">
<bpmn:text>This start event doesn't go anywhere!  that should raise a sensible error to the ui</bpmn:text>
</bpmn:textAnnotation>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_18biih5">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="189" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0u8fjmw_di">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1lmkn99_di" bpmnElement="SequenceFlow_1lmkn99">
<di:waypoint x="370" y="117" />
<di:waypoint x="425" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ExclusiveGateway_14wqqsi_di" bpmnElement="ExclusiveGateway_003amsm" isMarkerVisible="true">
<dc:Bounds x="425" y="182" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_12acevn_di" bpmnElement="SequenceFlow_No_Bananas">
<di:waypoint x="450" y="232" />
<di:waypoint x="450" y="320" />
<di:waypoint x="560" y="320" />
<bpmndi:BPMNLabel>
<dc:Bounds x="459" y="273" width="13" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_0ht939a_di" bpmnElement="Task_Num_Bananas">
<dc:Bounds x="560" y="167" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0gwjzr9_di" bpmnElement="Task_Why_No_Bananas">
<dc:Bounds x="560" y="280" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_063bpg6_di" bpmnElement="EndEvent_063bpg6">
<dc:Bounds x="752" y="189" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_02z84p5_di" bpmnElement="SequenceFlow_02z84p5">
<di:waypoint x="660" y="207" />
<di:waypoint x="752" y="207" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1hwtug4_di" bpmnElement="EndEvent_1hwtug4">
<dc:Bounds x="752" y="302" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_08djf6q_di" bpmnElement="SequenceFlow_08djf6q">
<di:waypoint x="660" y="320" />
<di:waypoint x="752" y="320" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1pnq3kg_di" bpmnElement="SequenceFlow_1pnq3kg">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0f3vx1l_di" bpmnElement="SequenceFlow_Yes_Bananas">
<di:waypoint x="475" y="207" />
<di:waypoint x="560" y="207" />
<bpmndi:BPMNLabel>
<dc:Bounds x="509" y="189" width="18" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TextAnnotation_014touo_di" bpmnElement="TextAnnotation_014touo">
<dc:Bounds x="220" y="80" width="100" height="96" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -31,6 +31,7 @@ class TestTasksApi(BaseTest):
(workflow.id, str(soft_reset), str(hard_reset)),
headers=self.logged_in_headers(),
content_type="application/json")
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
workflow_api = WorkflowApiSchema().load(json_data)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
@ -243,6 +244,32 @@ class TestTasksApi(BaseTest):
self.assertTrue(workflow_api.spec_version.startswith("v2 "))
self.assertTrue(workflow_api.is_latest_spec)
def test_soft_reset_errors_out_and_next_result_is_on_original_version(self):
# Start the basic two_forms workflow and complete a task.
self.load_example_data()
workflow = self.create_workflow('two_forms')
workflow_api = self.get_workflow_api(workflow)
self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"})
self.assertTrue(workflow_api.is_latest_spec)
# Modify the specification, with a major change that alters the flow and can't be deserialized
# effectively, if it uses the latest spec files.
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'two_forms', 'mods', 'two_forms_struc_mod.bpmn')
self.replace_file("two_forms.bpmn", file_path)
# perform a soft reset returns an error
rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' %
(workflow.id, "true", "false"),
content_type="application/json",
headers=self.logged_in_headers())
self.assert_failure(rv, error_code="unexpected_workflow_structure")
# Try again without a soft reset, and we are still ok, and on the original version.
workflow_api = self.get_workflow_api(workflow)
self.assertTrue(workflow_api.spec_version.startswith("v1 "))
self.assertFalse(workflow_api.is_latest_spec)
def test_get_workflow_stats(self):
self.load_example_data()
workflow = self.create_workflow('exclusive_gateway')

View File

@ -159,6 +159,42 @@ class TestWorkflowProcessor(BaseTest):
self.assertIn("details", task.data)
self.assertIsInstance(task.task_spec, EndEvent)
def test_workflow_validation_error_is_properly_raised(self):
self.load_example_data()
workflow_spec_model = self.load_test_spec("invalid_spec")
study = session.query(StudyModel).first()
with self.assertRaises(ApiError) as context:
WorkflowProcessor.create(study.id, workflow_spec_model.id)
self.assertEquals("workflow_validation_error", context.exception.code)
self.assertTrue("bpmn:startEvent" in context.exception.message)
def test_workflow_spec_key_error(self):
"""Frequently seeing errors in the logs about a 'Key' error, where a workflow
references something that doesn't exist in the midst of processing. Want to
make sure we produce errors to the front end that allows us to debug this."""
# Start the two_forms workflow, and enter some data in the first form.
self.load_example_data()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("two_forms")
processor = WorkflowProcessor.create(study.id, workflow_spec_model.id)
workflow_model = db.session.query(WorkflowModel).filter(WorkflowModel.study_id == study.id).first()
self.assertEqual(workflow_model.workflow_spec_id, workflow_spec_model.id)
task = processor.next_task()
task.data = {"color": "blue"}
processor.complete_task(task)
# Modify the specification, with a major change.
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'two_forms', 'mods', 'two_forms_struc_mod.bpmn')
self.replace_file("two_forms.bpmn", file_path)
# Attemping a soft update on a structural change should raise a sensible error.
with self.assertRaises(ApiError) as context:
processor3 = WorkflowProcessor(workflow_model, soft_reset=True)
self.assertEqual("unexpected_workflow_structure", context.exception.code)
def test_workflow_with_bad_expression_raises_sensible_error(self):
self.load_example_data()