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:
parent
78b6f040eb
commit
70611e2c1d
|
@ -6,9 +6,10 @@ from flask import g
|
||||||
from crc import session, auth
|
from crc import session, auth
|
||||||
from crc.api.common import ApiError, ApiErrorSchema
|
from crc.api.common import ApiError, ApiErrorSchema
|
||||||
from crc.api.workflow import __get_workflow_api_model
|
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.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
|
||||||
from crc.models.study import StudyModelSchema, StudyModel
|
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.workflow_processor import WorkflowProcessor
|
||||||
from crc.services.protocol_builder import ProtocolBuilderService
|
from crc.services.protocol_builder import ProtocolBuilderService
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import uuid
|
||||||
from crc.api.file import delete_file
|
from crc.api.file import delete_file
|
||||||
from crc import session
|
from crc import session
|
||||||
from crc.api.common import ApiError, ApiErrorSchema
|
from crc.api.common import ApiError, ApiErrorSchema
|
||||||
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, \
|
from crc.models.api_models import Task, WorkflowApi, WorkflowApiSchema
|
||||||
Task, WorkflowApiSchema, WorkflowApi
|
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel
|
||||||
from crc.services.workflow_processor import WorkflowProcessor
|
from crc.services.workflow_processor import WorkflowProcessor
|
||||||
from crc.models.file import FileModel
|
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),
|
last_task=Task.from_spiff(processor.bpmn_workflow.last_task),
|
||||||
next_task=None,
|
next_task=None,
|
||||||
user_tasks=user_tasks,
|
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():
|
if processor.next_task():
|
||||||
workflow_api.next_task = Task.from_spiff(processor.next_task())
|
workflow_api.next_task = Task.from_spiff(processor.next_task())
|
||||||
return workflow_api
|
return workflow_api
|
||||||
|
|
||||||
|
|
||||||
def get_workflow(workflow_id):
|
def get_workflow(workflow_id):
|
||||||
schema = WorkflowApiSchema()
|
schema = WorkflowApiSchema()
|
||||||
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
|
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
|
||||||
|
|
|
@ -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)
|
|
@ -1,14 +1,8 @@
|
||||||
import enum
|
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 marshmallow_sqlalchemy import ModelSchema
|
||||||
|
|
||||||
from crc import db, ma
|
from crc import db
|
||||||
from crc.api.common import ApiError
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowSpecModel(db.Model):
|
class WorkflowSpecModel(db.Model):
|
||||||
|
@ -41,112 +35,3 @@ class WorkflowModel(db.Model):
|
||||||
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
|
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)
|
|
||||||
|
|
|
@ -103,10 +103,11 @@ class WorkflowProcessor(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_spec(workflow_spec_id):
|
def get_spec(workflow_spec_id):
|
||||||
"""Returns the last version of the specification."""
|
"""Returns the latest version of the specification."""
|
||||||
parser = WorkflowProcessor.get_parser()
|
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.
|
minor_version = [] # The versions of the minor files if any.
|
||||||
|
file_ids = []
|
||||||
process_id = None
|
process_id = None
|
||||||
file_data_models = session.query(FileDataModel) \
|
file_data_models = session.query(FileDataModel) \
|
||||||
.join(FileModel) \
|
.join(FileModel) \
|
||||||
|
@ -129,7 +130,7 @@ class WorkflowProcessor(object):
|
||||||
minor_version.append(file_data.version)
|
minor_version.append(file_data.version)
|
||||||
if process_id is None:
|
if process_id is None:
|
||||||
raise(Exception("There is no primary BPMN model defined for workflow %s" % workflow_spec_id))
|
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 = parser.get_spec(process_id)
|
||||||
spec.description = ".".join(str(x) for x in minor_version)
|
spec.description = ".".join(str(x) for x in minor_version)
|
||||||
return spec
|
return spec
|
||||||
|
|
|
@ -99,6 +99,9 @@ class ExampleDataLoader:
|
||||||
content_type = CONTENT_TYPES[file_extension[1:]]
|
content_type = CONTENT_TYPES[file_extension[1:]]
|
||||||
file_service.add_workflow_spec_file(workflow_spec=spec, name=filename, content_type=content_type,
|
file_service.add_workflow_spec_file(workflow_spec=spec, name=filename, content_type=content_type,
|
||||||
binary_data=data, primary=is_primary)
|
binary_data=data, primary=is_primary)
|
||||||
|
except IsADirectoryError as de:
|
||||||
|
# Ignore sub directories
|
||||||
|
pass
|
||||||
finally:
|
finally:
|
||||||
file.close()
|
file.close()
|
||||||
return spec
|
return spec
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -3,10 +3,10 @@ from datetime import datetime, timezone
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from crc import session
|
from crc import session
|
||||||
|
from crc.models.api_models import WorkflowApiSchema
|
||||||
from crc.models.study import StudyModel, StudyModelSchema
|
from crc.models.study import StudyModel, StudyModelSchema
|
||||||
from crc.models.protocol_builder import ProtocolBuilderStatus
|
from crc.models.protocol_builder import ProtocolBuilderStatus
|
||||||
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \
|
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus
|
||||||
WorkflowApiSchema
|
|
||||||
from tests.base_test import BaseTest
|
from tests.base_test import BaseTest
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
|
||||||
|
|
||||||
from crc import session, db, app
|
from crc import session, db, app
|
||||||
from crc.api.common import ApiError
|
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.study import StudyModel
|
||||||
from crc.models.workflow import WorkflowSpecModel, WorkflowStatus, WorkflowModel
|
from crc.models.workflow import WorkflowSpecModel, WorkflowStatus, WorkflowModel
|
||||||
from crc.services.file_service import FileService
|
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.assertNotEqual(task.get_name(), task_before_restart.get_name())
|
||||||
self.assertEqual(task.get_name(), task_after_restart.get_name())
|
self.assertEqual(task.get_name(), task_after_restart.get_name())
|
||||||
self.assertEqual(task.data, task_after_restart.data)
|
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)
|
||||||
|
|
Loading…
Reference in New Issue