Adding the version of the specification used to create a workflow to the workflow api endpoint. Though the exact content of this version is likely to change.

Split the API specific models out from the workflow models to help me keep this straight.
Added tests to help me understand the errors thrown the and resolution path when a workflow specification changes in the midst of a running workflow.
This commit is contained in:
Dan Funk 2020-03-05 11:18:20 -05:00
parent 78b6f040eb
commit 70611e2c1d
10 changed files with 345 additions and 127 deletions

View File

@ -6,9 +6,10 @@ from flask import g
from crc import session, auth
from crc.api.common import ApiError, ApiErrorSchema
from crc.api.workflow import __get_workflow_api_model
from crc.models.api_models import WorkflowApiSchema
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
from crc.models.study import StudyModelSchema, StudyModel
from crc.models.workflow import WorkflowModel, WorkflowApiSchema, WorkflowSpecModel, WorkflowApi
from crc.models.workflow import WorkflowModel, WorkflowSpecModel
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.protocol_builder import ProtocolBuilderService

View File

@ -3,8 +3,8 @@ import uuid
from crc.api.file import delete_file
from crc import session
from crc.api.common import ApiError, ApiErrorSchema
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, \
Task, WorkflowApiSchema, WorkflowApi
from crc.models.api_models import Task, WorkflowApi, WorkflowApiSchema
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel
from crc.services.workflow_processor import WorkflowProcessor
from crc.models.file import FileModel
@ -74,12 +74,14 @@ def __get_workflow_api_model(processor: WorkflowProcessor):
last_task=Task.from_spiff(processor.bpmn_workflow.last_task),
next_task=None,
user_tasks=user_tasks,
workflow_spec_id=processor.workflow_spec_id
workflow_spec_id=processor.workflow_spec_id,
spec_version=processor.get_spec_version()
)
if processor.next_task():
workflow_api.next_task = Task.from_spiff(processor.next_task())
return workflow_api
def get_workflow(workflow_id):
schema = WorkflowApiSchema()
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()

120
crc/models/api_models.py Normal file
View File

@ -0,0 +1,120 @@
import jinja2
import marshmallow
from jinja2 import Template
from marshmallow import INCLUDE
from marshmallow_enum import EnumField
from crc import ma
from crc.api.common import ApiError
from crc.models.workflow import WorkflowStatus
class Task(object):
def __init__(self, id, name, title, type, state, form, documentation, data):
self.id = id
self.name = name
self.title = title
self.type = type
self.state = state
self.form = form
self.documentation = documentation
self.data = data
@classmethod
def from_spiff(cls, spiff_task):
documentation = spiff_task.task_spec.documentation if hasattr(spiff_task.task_spec, "documentation") else ""
instance = cls(spiff_task.id,
spiff_task.task_spec.name,
spiff_task.task_spec.description,
spiff_task.task_spec.__class__.__name__,
spiff_task.get_state_name(),
None,
documentation,
spiff_task.data)
if hasattr(spiff_task.task_spec, "form"):
instance.form = spiff_task.task_spec.form
if documentation != "" and documentation is not None:
instance.process_documentation(documentation)
return instance
def process_documentation(self, documentation):
'''Runs markdown documentation through the Jinja2 processor to inject data
create loops, etc...'''
template = Template(documentation)
try:
self.documentation = template.render(**self.data)
except jinja2.exceptions.UndefinedError as ue:
raise ApiError(code="template_error", message="Error processing template for task %s: %s" %
(self.name, str(ue)), status_code=500)
class OptionSchema(ma.Schema):
class Meta:
fields = ["id", "name"]
class ValidationSchema(ma.Schema):
class Meta:
fields = ["name", "config"]
class PropertiesSchema(ma.Schema):
class Meta:
fields = ["id", "value"]
class FormFieldSchema(ma.Schema):
class Meta:
fields = [
"id", "type", "label", "default_value", "options", "validation", "properties", "value"
]
default_value = marshmallow.fields.String(required=False, allow_none=True)
options = marshmallow.fields.List(marshmallow.fields.Nested(OptionSchema))
validation = marshmallow.fields.List(marshmallow.fields.Nested(ValidationSchema))
properties = marshmallow.fields.List(marshmallow.fields.Nested(PropertiesSchema))
class FormSchema(ma.Schema):
key = marshmallow.fields.String(required=True, allow_none=False)
fields = marshmallow.fields.List(marshmallow.fields.Nested(FormFieldSchema))
class TaskSchema(ma.Schema):
class Meta:
fields = ["id", "name", "title", "type", "state", "form", "documentation", "data"]
documentation = marshmallow.fields.String(required=False, allow_none=True)
form = marshmallow.fields.Nested(FormSchema, required=False, allow_none=True)
title = marshmallow.fields.String(required=False, allow_none=True)
@marshmallow.post_load
def make_task(self, data, **kwargs):
return Task(**data)
class WorkflowApi(object):
def __init__(self, id, status, user_tasks, last_task, next_task, workflow_spec_id, spec_version):
self.id = id
self.status = status
self.user_tasks = user_tasks
self.last_task = last_task
self.next_task = next_task
self.workflow_spec_id = workflow_spec_id
self.spec_version = spec_version
class WorkflowApiSchema(ma.Schema):
class Meta:
model = WorkflowApi
fields = ["id", "status", "user_tasks", "last_task", "next_task", "workflow_spec_id", "spec_version"]
unknown = INCLUDE
status = EnumField(WorkflowStatus)
user_tasks = marshmallow.fields.List(marshmallow.fields.Nested(TaskSchema, dump_only=True))
last_task = marshmallow.fields.Nested(TaskSchema, dump_only=True)
next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False)
@marshmallow.post_load
def make_workflow(self, data, **kwargs):
return WorkflowApi(**data)

View File

@ -1,14 +1,8 @@
import enum
import jinja2
import marshmallow
from jinja2 import Template
from marshmallow import INCLUDE
from marshmallow_enum import EnumField
from marshmallow_sqlalchemy import ModelSchema
from crc import db, ma
from crc.api.common import ApiError
from crc import db
class WorkflowSpecModel(db.Model):
@ -41,112 +35,3 @@ class WorkflowModel(db.Model):
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
class Task(object):
def __init__(self, id, name, title, type, state, form, documentation, data):
self.id = id
self.name = name
self.title = title
self.type = type
self.state = state
self.form = form
self.documentation = documentation
self.data = data
@classmethod
def from_spiff(cls, spiff_task):
documentation = spiff_task.task_spec.documentation if hasattr(spiff_task.task_spec, "documentation") else ""
instance = cls(spiff_task.id,
spiff_task.task_spec.name,
spiff_task.task_spec.description,
spiff_task.task_spec.__class__.__name__,
spiff_task.get_state_name(),
None,
documentation,
spiff_task.data)
if hasattr(spiff_task.task_spec, "form"):
instance.form = spiff_task.task_spec.form
if documentation != "" and documentation is not None:
instance.process_documentation(documentation)
return instance
def process_documentation(self, documentation):
'''Runs markdown documentation through the Jinja2 processor to inject data
create loops, etc...'''
template = Template(documentation)
try:
self.documentation = template.render(**self.data)
except jinja2.exceptions.UndefinedError as ue:
raise ApiError(code="template_error", message="Error processing template for task %s: %s" %
(self.name, str(ue)), status_code=500)
class OptionSchema(ma.Schema):
class Meta:
fields = ["id", "name"]
class ValidationSchema(ma.Schema):
class Meta:
fields = ["name", "config"]
class PropertiesSchema(ma.Schema):
class Meta:
fields = ["id", "value"]
class FormFieldSchema(ma.Schema):
class Meta:
fields = [
"id", "type", "label", "default_value", "options", "validation", "properties", "value"
]
default_value = marshmallow.fields.String(required=False, allow_none=True)
options = marshmallow.fields.List(marshmallow.fields.Nested(OptionSchema))
validation = marshmallow.fields.List(marshmallow.fields.Nested(ValidationSchema))
properties = marshmallow.fields.List(marshmallow.fields.Nested(PropertiesSchema))
class FormSchema(ma.Schema):
key = marshmallow.fields.String(required=True, allow_none=False)
fields = marshmallow.fields.List(marshmallow.fields.Nested(FormFieldSchema))
class TaskSchema(ma.Schema):
class Meta:
fields = ["id", "name", "title", "type", "state", "form", "documentation", "data"]
documentation = marshmallow.fields.String(required=False, allow_none=True)
form = marshmallow.fields.Nested(FormSchema, required=False, allow_none=True)
title = marshmallow.fields.String(required=False, allow_none=True)
@marshmallow.post_load
def make_task(self, data, **kwargs):
return Task(**data)
class WorkflowApi(object):
def __init__(self, id, status, user_tasks, last_task, next_task, workflow_spec_id):
self.id = id
self.status = status
self.user_tasks = user_tasks
self.last_task = last_task
self.next_task = next_task
self.workflow_spec_id = workflow_spec_id
class WorkflowApiSchema(ma.Schema):
class Meta:
model = WorkflowApi
fields = ["id", "status", "user_tasks", "last_task", "next_task", "workflow_spec_id"]
unknown = INCLUDE
status = EnumField(WorkflowStatus)
user_tasks = marshmallow.fields.List(marshmallow.fields.Nested(TaskSchema, dump_only=True))
last_task = marshmallow.fields.Nested(TaskSchema, dump_only=True)
next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False)
@marshmallow.post_load
def make_workflow(self, data, **kwargs):
return WorkflowApi(**data)

View File

@ -103,10 +103,11 @@ class WorkflowProcessor(object):
@staticmethod
def get_spec(workflow_spec_id):
"""Returns the last version of the specification."""
"""Returns the latest version of the specification."""
parser = WorkflowProcessor.get_parser()
major_version = 0 # The version of the pirmary file.
major_version = 0 # The version of the primary file.
minor_version = [] # The versions of the minor files if any.
file_ids = []
process_id = None
file_data_models = session.query(FileDataModel) \
.join(FileModel) \
@ -129,7 +130,7 @@ class WorkflowProcessor(object):
minor_version.append(file_data.version)
if process_id is None:
raise(Exception("There is no primary BPMN model defined for workflow %s" % workflow_spec_id))
minor_version.insert(0, major_version) # Add major version to begining.
minor_version.insert(0, major_version) # Add major version to beginning.
spec = parser.get_spec(process_id)
spec.description = ".".join(str(x) for x in minor_version)
return spec

View File

@ -99,6 +99,9 @@ class ExampleDataLoader:
content_type = CONTENT_TYPES[file_extension[1:]]
file_service.add_workflow_spec_file(workflow_spec=spec, name=filename, content_type=content_type,
binary_data=data, primary=is_primary)
except IsADirectoryError as de:
# Ignore sub directories
pass
finally:
file.close()
return spec

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_06g9dcb" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_1giz8il" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0myefwb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0myefwb" sourceRef="StartEvent_1" targetRef="StepOne" />
<bpmn:sequenceFlow id="SequenceFlow_00p5po6" sourceRef="StepOne" targetRef="Task_1i59nh4" />
<bpmn:endEvent id="EndEvent_1gsujvg">
<bpmn:incoming>SequenceFlow_17ggqu2</bpmn:incoming>
</bpmn:endEvent>
<bpmn:userTask id="StepOne" name="Step 1" camunda:formKey="StepOneForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="color" label="What is your favorite color?" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0myefwb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_00p5po6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_17ggqu2" sourceRef="Task_1i59nh4" targetRef="EndEvent_1gsujvg" />
<bpmn:userTask id="Task_1i59nh4" name="New Step" camunda:formKey="MyNewForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="FormField_01vbdk5" label="I forgot to ask you about this, what is your quest?" type="string" defaultValue="To seak the holy grail!" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_00p5po6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ggqu2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:textAnnotation id="TextAnnotation_1haj11l">
<bpmn:text>We have a test that replaces tow_forms with this file, which adds a new step to the process.  A breaking change.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_02qm351" sourceRef="Task_1i59nh4" targetRef="TextAnnotation_1haj11l" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1giz8il">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="279" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0myefwb_di" bpmnElement="SequenceFlow_0myefwb">
<di:waypoint x="215" y="297" />
<di:waypoint x="270" y="297" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_00p5po6_di" bpmnElement="SequenceFlow_00p5po6">
<di:waypoint x="370" y="297" />
<di:waypoint x="420" y="297" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1gsujvg_di" bpmnElement="EndEvent_1gsujvg">
<dc:Bounds x="712" y="279" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1xakn8i_di" bpmnElement="StepOne">
<dc:Bounds x="270" y="257" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_17ggqu2_di" bpmnElement="SequenceFlow_17ggqu2">
<di:waypoint x="520" y="297" />
<di:waypoint x="712" y="297" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_1tw6x6h_di" bpmnElement="Task_1i59nh4">
<dc:Bounds x="420" y="257" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1haj11l_di" bpmnElement="TextAnnotation_1haj11l">
<dc:Bounds x="540" y="80" width="169" height="136" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_02qm351_di" bpmnElement="Association_02qm351">
<di:waypoint x="511" y="257" />
<di:waypoint x="554" y="216" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,72 @@
<?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_06g9dcb" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_1giz8il" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0myefwb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0myefwb" sourceRef="StartEvent_1" targetRef="StepOne" />
<bpmn:sequenceFlow id="SequenceFlow_00p5po6" sourceRef="StepOne" targetRef="StepTwo" />
<bpmn:endEvent id="EndEvent_1gsujvg">
<bpmn:incoming>SequenceFlow_0huye14</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0huye14" sourceRef="StepTwo" targetRef="EndEvent_1gsujvg" />
<bpmn:userTask id="StepOne" name="Step 1" camunda:formKey="StepOneForm">
<bpmn:documentation># This is some documentation I wanted to add.</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="color" label="What is your favorite color?" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0myefwb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_00p5po6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="StepTwo" name="Step 2" camunda:formKey="StepTwoForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="capital" label="What is the capital of Assyria?" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_00p5po6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0huye14</bpmn:outgoing>
</bpmn:userTask>
<bpmn:textAnnotation id="TextAnnotation_0uiis6p">
<bpmn:text>There is a minor text change to the description here.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1nt50pu" sourceRef="StepOne" targetRef="TextAnnotation_0uiis6p" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1giz8il">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="219" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0myefwb_di" bpmnElement="SequenceFlow_0myefwb">
<di:waypoint x="215" y="237" />
<di:waypoint x="270" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_00p5po6_di" bpmnElement="SequenceFlow_00p5po6">
<di:waypoint x="370" y="237" />
<di:waypoint x="430" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1gsujvg_di" bpmnElement="EndEvent_1gsujvg">
<dc:Bounds x="592" y="219" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0huye14_di" bpmnElement="SequenceFlow_0huye14">
<di:waypoint x="530" y="237" />
<di:waypoint x="592" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_1xakn8i_di" bpmnElement="StepOne">
<dc:Bounds x="270" y="197" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0fltcd6_di" bpmnElement="StepTwo">
<dc:Bounds x="430" y="197" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_0uiis6p_di" bpmnElement="TextAnnotation_0uiis6p">
<dc:Bounds x="370" y="80" width="100" height="82" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1nt50pu_di" bpmnElement="Association_1nt50pu">
<di:waypoint x="354" y="197" />
<di:waypoint x="385" y="162" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -3,10 +3,10 @@ from datetime import datetime, timezone
from unittest.mock import patch
from crc import session
from crc.models.api_models import WorkflowApiSchema
from crc.models.study import StudyModel, StudyModelSchema
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \
WorkflowApiSchema
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus
from tests.base_test import BaseTest

View File

@ -7,7 +7,7 @@ from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
from crc import session, db, app
from crc.api.common import ApiError
from crc.models.file import FileModel, FileDataModel
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowSpecModel, WorkflowStatus, WorkflowModel
from crc.services.file_service import FileService
@ -254,3 +254,66 @@ class TestWorkflowProcessor(BaseTest):
self.assertNotEqual(task.get_name(), task_before_restart.get_name())
self.assertEqual(task.get_name(), task_after_restart.get_name())
self.assertEqual(task.data, task_after_restart.data)
def replace_file(self, name, file_path):
"""Replaces a stored file with the given name with the contents of the file at the given path."""
file_service = FileService()
file = open(file_path, "rb")
data = file.read()
file_model = db.session.query(FileModel).filter(FileModel.name == name).first()
noise, file_extension = os.path.splitext(file_path)
content_type = CONTENT_TYPES[file_extension[1:]]
file_service.update_file(file_model, data, content_type)
def test_modify_spec_with_text_change_with_running_workflow(self):
self.load_example_data()
# Start the two_forms workflow, and enter some data in the first form.
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 minor text change.
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'two_forms', 'mods', 'two_forms_text_mod.bpmn')
self.replace_file("two_forms.bpmn", file_path)
processor2 = WorkflowProcessor(workflow_spec_model.id, processor.serialize())
self.assertEquals("Step 1", processor2.bpmn_workflow.last_task.task_spec.description)
self.assertEquals("# This is some documentation I wanted to add.",
processor2.bpmn_workflow.last_task.task_spec.documentation)
def test_modify_spec_with_structural_change_with_running_workflow(self):
self.load_example_data()
# Start the two_forms workflow, and enter some data in the first form.
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)
next_task = processor.next_task()
self.assertEquals("Step 2", next_task.task_spec.description)
# Modify the specification, with a major change that alters the flow and can't be serialized effectively.
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)
with self.assertRaises(KeyError):
processor2 = WorkflowProcessor(workflow_spec_model.id, processor.serialize())
# Restart the workflow, and the error should go away
processor.restart_with_current_task_data()
self.assertEquals("Step 1", processor.next_task().task_spec.description)
processor.complete_task(processor.next_task())
self.assertEquals("New Step", processor.next_task().task_spec.description)
self.assertEquals({"color": "blue"}, processor.next_task().data)