added test to create a spec file w/ burnettk

This commit is contained in:
jasquat 2022-05-25 16:38:03 -04:00
parent eb082fe01c
commit 86938792fd
15 changed files with 869 additions and 37 deletions

2
poetry.lock generated
View File

@ -1948,7 +1948,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "5d1c5760fc1a25e81b1bea11877c16480113adce42eb4b2ebdeb3df730665419"
content-hash = "d0f2edc038a129a994d9b2d2fd9a30df000450d062ef183316197f54775fce5a"
[metadata.files]
alabaster = [

View File

@ -38,6 +38,7 @@ pytest-flask-sqlalchemy = "^1.1.0"
psycopg2 = "^2.9.3"
typing-extensions = "^4.2.0"
connexion = "^2.13.1"
lxml = "^4.8.0"
[tool.poetry.dev-dependencies]

View File

@ -54,6 +54,111 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/WorkflowSpec"
/workflow-specification/{spec_id}/file:
parameters:
- name: spec_id
in: path
required: true
description: The unique id of an existing workflow specification to validate.
schema:
type: string
# get:
# operationId: spiff_workflow_webapp.api.process_api_blueprint.get_files
# summary: Provide a list of workflow spec files for the given workflow_spec_id. IMPORTANT, only includes metadata, not the file content.
# tags:
# - Spec Files
# responses:
# '200':
# description: An array of file descriptions (not the file content)
# content:
# application/json:
# schema:
# type: array
# items:
# $ref: "#/components/schemas/File"
post:
operationId: spiff_workflow_webapp.routes.process_api_blueprint.add_file
summary: Add a new workflow spec file
tags:
- Spec Files
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
'200':
description: Metadata about the uploaded file, but not the file content.
content:
application/json:
schema:
$ref: "#components/schemas/File"
/workflow-specification/{spec_id}/file/{file_name}:
parameters:
- name: spec_id
in: path
required: true
description: The unique id of an existing workflow specification to validate.
schema:
type: string
- name: file_name
in: path
required: true
description: The id of the spec file
schema:
type: string
get:
operationId: spiff_workflow_webapp.routes.process_api_blueprint.get_file
summary: Returns metadata about the file
tags:
- Spec Files
responses:
'200':
description: Returns the file information requested.
content:
application/json:
schema:
$ref: "#components/schemas/File"
# put:
# operationId: crc.api.spec_file.update
# summary: updates the given file to be the primary file and process, if so specified.
# tags:
# - Spec Files
# parameters:
# - name: is_primary
# in: query
# required: true
# description: Whether to make this the primary file for the workflow.
# schema:
# type: boolean
# requestBody:
# description: Log Pagination Request
# required: false
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/File'
# responses:
# '200':
# description: Returns the file information.
# content:
# application/json:
# schema:
# $ref: "#components/schemas/File"
# delete:
# operationId: crc.api.spec_file.delete
# summary: Removes an existing workflow spec file.
# tags:
# - Spec Files
# responses:
# '204':
# description: The file was removed.
components:

View File

@ -1,5 +1,6 @@
"""File."""
import enum
from marshmallow import Schema, INCLUDE
class FileType(enum.Enum):
@ -82,3 +83,25 @@ class File(object):
# fixme: How to track the user id?
instance.data_store = {}
return instance
class FileSchema(Schema):
class Meta:
model = File
fields = ["id", "name", "content_type", "workflow_id",
"irb_doc_code", "last_modified", "type", "archived",
"size", "data_store", "document", "user_uid", "url"]
unknown = INCLUDE
# url = Method("get_url")
#
# def get_url(self, obj):
# token = 'not_available'
# if hasattr(obj, 'id') and obj.id is not None:
# file_url = url_for("/v1_0.crc_api_file_get_file_data_link", file_id=obj.id, _external=True)
# if hasattr(flask.g, 'user'):
# token = flask.g.user.encode_auth_token()
# url = file_url + '?auth_token=' + urllib.parse.quote_plus(token)
# return url
# else:
# return ""
#

View File

@ -1,15 +1,18 @@
"""APIs for dealing with process groups, process models, and process instances."""
import connexion
from flask import Blueprint
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore
from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
# from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
# from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore
# from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.process_model import ProcessModelInfoSchema
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
from spiff_workflow_webapp.models.file import FileSchema, FileType
# from spiff_workflow_webapp.spiff_workflow_connector import parse
# from spiff_workflow_webapp.spiff_workflow_connector import run
@ -80,6 +83,28 @@ def add_workflow_specification(body):
return ProcessModelInfoSchema().dump(spec)
def get_file(spec_id, file_name):
workflow_spec_service = ProcessModelService()
workflow_spec = workflow_spec_service.get_spec(spec_id)
files = SpecFileService.get_files(workflow_spec, file_name)
if len(files) == 0:
raise ApiError(code='unknown file',
message=f'No information exists for file {file_name}'
f' it does not exist in workflow {spec_id}.', status_code=404)
return FileSchema().dump(files[0])
def add_file(spec_id):
workflow_spec_service = ProcessModelService()
workflow_spec = workflow_spec_service.get_spec(spec_id)
file = connexion.request.files['file']
file = SpecFileService.add_file(workflow_spec, file.filename, file.stream.read())
if not workflow_spec.primary_process_id and file.type == FileType.bpmn.value:
SpecFileService.set_primary_bpmn(workflow_spec, file.name)
workflow_spec_service.update_spec(workflow_spec)
return FileSchema().dump(file)
# def get_workflow_specification(spec_id):
# """Get_workflow_specification."""
# spec_service = ProcessModelService()

View File

@ -0,0 +1,160 @@
import datetime
import os
import shutil
from typing import List
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.file import File, FileType
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from lxml import etree
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.services.file_system_service import FileSystemService
class SpecFileService(FileSystemService):
"""We store spec files on the file system. This allows us to take advantage of Git for
syncing and versioning.
The files are stored in a directory whose path is determined by the category and spec names.
"""
@staticmethod
def get_files(workflow_spec: ProcessModelInfo, file_name=None, include_libraries=False) -> List[File]:
""" Returns all files associated with a workflow specification """
path = SpecFileService.workflow_path(workflow_spec)
files = SpecFileService._get_files(path, file_name)
if include_libraries:
for lib_name in workflow_spec.libraries:
lib_path = SpecFileService.library_path(lib_name)
files.extend(SpecFileService._get_files(lib_path, file_name))
return files
@staticmethod
def add_file(workflow_spec: ProcessModelInfo, file_name: str, binary_data: bytearray) -> File:
# Same as update
return SpecFileService.update_file(workflow_spec, file_name, binary_data)
@staticmethod
def update_file(workflow_spec: ProcessModelInfo, file_name: str, binary_data) -> File:
SpecFileService.assert_valid_file_name(file_name)
file_path = SpecFileService.file_path(workflow_spec, file_name)
SpecFileService.write_file_data_to_system(file_path, binary_data)
file = SpecFileService.to_file_object(file_name, file_path)
if file_name == workflow_spec.primary_file_name:
SpecFileService.set_primary_bpmn(workflow_spec, file_name, binary_data)
elif workflow_spec.primary_file_name is None and file.type == FileType.bpmn:
# If no primary process exists, make this pirmary process.
SpecFileService.set_primary_bpmn(workflow_spec, file_name, binary_data)
return file
@staticmethod
def get_data(workflow_spec: ProcessModelInfo, file_name: str):
file_path = SpecFileService.file_path(workflow_spec, file_name)
if not os.path.exists(file_path):
# If the file isn't here, it may be in a library
for lib in workflow_spec.libraries:
file_path = SpecFileService.library_path(lib)
file_path = os.path.join(file_path, file_name)
if os.path.exists(file_path):
break
if not os.path.exists(file_path):
raise ApiError("unknown_file", f"No file found with name {file_name} in {workflow_spec.display_name}")
with open(file_path, 'rb') as f_handle:
spec_file_data = f_handle.read()
return spec_file_data
@staticmethod
def file_path(spec: ProcessModelInfo, file_name: str):
return os.path.join(SpecFileService.workflow_path(spec), file_name)
@staticmethod
def last_modified(spec: ProcessModelInfo, file_name: str):
path = SpecFileService.file_path(spec, file_name)
return FileSystemService._last_modified(path)
@staticmethod
def timestamp(spec: ProcessModelInfo, file_name: str):
path = SpecFileService.file_path(spec, file_name)
return FileSystemService._timestamp(path)
@staticmethod
def delete_file(spec, file_name):
# Fixme: Remember to remove the lookup files when the spec file is removed.
# lookup_files = session.query(LookupFileModel).filter_by(file_model_id=file_id).all()
# for lf in lookup_files:
# session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete()
# session.query(LookupFileModel).filter_by(id=lf.id).delete()
file_path = SpecFileService.file_path(spec, file_name)
os.remove(file_path)
@staticmethod
def delete_all_files(spec):
dir_path = SpecFileService.workflow_path(spec)
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
@staticmethod
def set_primary_bpmn(workflow_spec: ProcessModelInfo, file_name: str, binary_data=None):
# If this is a BPMN, extract the process id, and determine if it is contains swim lanes.
extension = SpecFileService.get_extension(file_name)
file_type = FileType[extension]
if file_type == FileType.bpmn:
if not binary_data:
binary_data = SpecFileService.get_data(workflow_spec, file_name)
try:
bpmn: etree.Element = etree.fromstring(binary_data)
workflow_spec.primary_process_id = SpecFileService.get_process_id(bpmn)
workflow_spec.primary_file_name = file_name
workflow_spec.is_review = SpecFileService.has_swimlane(bpmn)
except etree.XMLSyntaxError as xse:
raise ApiError("invalid_xml", "Failed to parse xml: " + str(xse), file_name=file_name)
except ValidationException as ve:
if ve.args[0].find('No executable process tag found') >= 0:
raise ApiError(code='missing_executable_option',
message='No executable process tag found. Please make sure the Executable option is set in the workflow.')
else:
raise ApiError(code='validation_error',
message=f'There was an error validating your workflow. Original message is: {ve}')
else:
raise ApiError("invalid_xml", "Only a BPMN can be the primary file.", file_name=file_name)
@staticmethod
def has_swimlane(et_root: etree.Element):
"""
Look through XML and determine if there are any lanes present that have a label.
"""
elements = et_root.xpath('//bpmn:lane',
namespaces={'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL'})
retval = False
for el in elements:
if el.get('name'):
retval = True
return retval
@staticmethod
def get_process_id(et_root: etree.Element):
process_elements = []
for child in et_root:
if child.tag.endswith('process') and child.attrib.get('isExecutable', False):
process_elements.append(child)
if len(process_elements) == 0:
raise ValidationException('No executable process tag found')
# There are multiple root elements
if len(process_elements) > 1:
# Look for the element that has the startEvent in it
for e in process_elements:
this_element: etree.Element = e
for child_element in list(this_element):
if child_element.tag.endswith('startEvent'):
return this_element.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,200 @@
<?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_1gjhqt9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.0.0">
<bpmn:process id="Process_1ds61df" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0c7wlth</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Task_User_Select_Type" name="Set Type" camunda:formKey="Get A Random Fun Fact">
<bpmn:documentation># h1 Heading 8-)
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
## Horizontal Rules
___
---
***
## Typographic replacements
"double quotes" and 'single quotes'
## Emphasis
**This is bold text**
__This is bold text__
*This is italic text*
_This is italic text_
~~Strikethrough~~
## Blockquotes
&gt; Blockquotes can also be nested...
&gt;&gt; ...by using additional greater-than signs right next to each other...
&gt; &gt; &gt; ...or with spaces between arrows.
## Lists
Unordered
+ Create a list by starting a line with `+`, `-`, or `*`
+ Sub-lists are made by indenting 2 spaces:
- Marker character change forces new list start:
* Ac tristique libero volutpat at
+ Facilisis in pretium nisl aliquet
- Nulla volutpat aliquam velit
+ Very easy!
Ordered
1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit
3. Integer molestie lorem at massa
1. You can use sequential numbers...
1. ...or keep all the numbers as `1.`
Start numbering with offset:
57. foo
1. bar
## Tables
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
Right aligned columns
| Option | Description |
| ------:| -----------:|
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
## Links
[link text](http://dev.nodeca.com)
[link with title](http://nodeca.github.io/pica/demo/ "title text!")
Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
## Images
![Minion](https://octodex.github.com/images/minion.png)
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="type" label="'Type'" type="enum" defaultValue="&#39;cat&#39;">
<camunda:validation>
<camunda:constraint name="required" config="True" />
</camunda:validation>
<camunda:value id="norris" name="Chuck Norris" />
<camunda:value id="cat" name="Cat Fact" />
<camunda:value id="buzzword" name="Business Buzzword" />
</camunda:formField>
</camunda:formData>
<camunda:properties>
<camunda:property name="type" value="string" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0c7wlth</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0641sh6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Task_Get_Fact_From_API" name="Display Fact">
<bpmn:documentation />
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="Fact.type" />
</camunda:inputOutput>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0641sh6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0t29gjo</bpmn:outgoing>
<bpmn:script>FactService = fact_service()</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0u1cgrf">
<bpmn:documentation># Great Job!
You have completed the random fact generator.
You chose to receive a random fact of the type: "{{type}}"
Your random fact is:
{{details}}</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0t29gjo</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0c7wlth" sourceRef="StartEvent_1" targetRef="Task_User_Select_Type" />
<bpmn:sequenceFlow id="SequenceFlow_0641sh6" sourceRef="Task_User_Select_Type" targetRef="Task_Get_Fact_From_API" />
<bpmn:sequenceFlow id="SequenceFlow_0t29gjo" sourceRef="Task_Get_Fact_From_API" targetRef="EndEvent_0u1cgrf" />
<bpmn:textAnnotation id="TextAnnotation_09fq7kh">
<bpmn:text>User sets the Fact.type to cat, norris, or buzzword</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1cfasjp" sourceRef="Task_User_Select_Type" targetRef="TextAnnotation_09fq7kh" />
<bpmn:textAnnotation id="TextAnnotation_1234e5n">
<bpmn:text>Makes an API  call to get a fact of the required type.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1qirnyy" sourceRef="Task_Get_Fact_From_API" targetRef="TextAnnotation_1234e5n" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1ds61df">
<bpmndi:BPMNEdge id="SequenceFlow_0t29gjo_di" bpmnElement="SequenceFlow_0t29gjo">
<di:waypoint x="570" y="250" />
<di:waypoint x="692" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0641sh6_di" bpmnElement="SequenceFlow_0641sh6">
<di:waypoint x="370" y="250" />
<di:waypoint x="470" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0c7wlth_di" bpmnElement="SequenceFlow_0c7wlth">
<di:waypoint x="188" y="250" />
<di:waypoint x="270" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_186s7tw_di" bpmnElement="Task_User_Select_Type">
<dc:Bounds x="270" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_10keafb_di" bpmnElement="Task_Get_Fact_From_API">
<dc:Bounds x="470" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
<dc:Bounds x="692" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_09fq7kh_di" bpmnElement="TextAnnotation_09fq7kh">
<dc:Bounds x="330" y="116" width="99.99202297383536" height="68.28334396936822" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1234e5n_di" bpmnElement="TextAnnotation_1234e5n">
<dc:Bounds x="570" y="120" width="100" height="68" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1cfasjp_di" bpmnElement="Association_1cfasjp">
<di:waypoint x="344" y="210" />
<di:waypoint x="359" y="184" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1qirnyy_di" bpmnElement="Association_1qirnyy">
<di:waypoint x="561" y="210" />
<di:waypoint x="584" y="188" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,200 @@
<?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_1gjhqt9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_SecondFact" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0c7wlth</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Task_User_Select_Type" name="Set Type" camunda:formKey="Get A Random Fun Fact">
<bpmn:documentation># h1 Heading 8-)
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
## Horizontal Rules
___
---
***
## Typographic replacements
"double quotes" and 'single quotes'
## Emphasis
**This is bold text**
__This is bold text__
*This is italic text*
_This is italic text_
~~Strikethrough~~
## Blockquotes
&gt; Blockquotes can also be nested...
&gt;&gt; ...by using additional greater-than signs right next to each other...
&gt; &gt; &gt; ...or with spaces between arrows.
## Lists
Unordered
+ Create a list by starting a line with `+`, `-`, or `*`
+ Sub-lists are made by indenting 2 spaces:
- Marker character change forces new list start:
* Ac tristique libero volutpat at
+ Facilisis in pretium nisl aliquet
- Nulla volutpat aliquam velit
+ Very easy!
Ordered
1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit
3. Integer molestie lorem at massa
1. You can use sequential numbers...
1. ...or keep all the numbers as `1.`
Start numbering with offset:
57. foo
1. bar
## Tables
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
Right aligned columns
| Option | Description |
| ------:| -----------:|
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
## Links
[link text](http://dev.nodeca.com)
[link with title](http://nodeca.github.io/pica/demo/ "title text!")
Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
## Images
![Minion](https://octodex.github.com/images/minion.png)
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="type" label="'Type'" type="enum" defaultValue="cat">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="norris" name="Chuck Norris" />
<camunda:value id="cat" name="Cat Fact" />
<camunda:value id="buzzword" name="Business Buzzword" />
</camunda:formField>
</camunda:formData>
<camunda:properties>
<camunda:property name="type" value="string" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0c7wlth</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0641sh6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Task_Get_Fact_From_API" name="Display Fact">
<bpmn:documentation />
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="Fact.type" />
</camunda:inputOutput>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0641sh6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0t29gjo</bpmn:outgoing>
<bpmn:script>FactService = fact_service()</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0u1cgrf">
<bpmn:documentation># Great Job!
You have completed the random fact generator.
You chose to receive a random fact of the type: "{{type}}"
Your random fact is:
{{details}}</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0t29gjo</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0c7wlth" sourceRef="StartEvent_1" targetRef="Task_User_Select_Type" />
<bpmn:sequenceFlow id="SequenceFlow_0641sh6" sourceRef="Task_User_Select_Type" targetRef="Task_Get_Fact_From_API" />
<bpmn:sequenceFlow id="SequenceFlow_0t29gjo" sourceRef="Task_Get_Fact_From_API" targetRef="EndEvent_0u1cgrf" />
<bpmn:textAnnotation id="TextAnnotation_09fq7kh">
<bpmn:text>User sets the Fact.type to cat, norris, or buzzword</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1cfasjp" sourceRef="Task_User_Select_Type" targetRef="TextAnnotation_09fq7kh" />
<bpmn:textAnnotation id="TextAnnotation_1234e5n">
<bpmn:text>Makes an API  call to get a fact of the required type.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1qirnyy" sourceRef="Task_Get_Fact_From_API" targetRef="TextAnnotation_1234e5n" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1ds61df">
<bpmndi:BPMNEdge id="SequenceFlow_0t29gjo_di" bpmnElement="SequenceFlow_0t29gjo">
<di:waypoint x="570" y="250" />
<di:waypoint x="692" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0641sh6_di" bpmnElement="SequenceFlow_0641sh6">
<di:waypoint x="370" y="250" />
<di:waypoint x="470" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0c7wlth_di" bpmnElement="SequenceFlow_0c7wlth">
<di:waypoint x="188" y="250" />
<di:waypoint x="270" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_186s7tw_di" bpmnElement="Task_User_Select_Type">
<dc:Bounds x="270" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
<dc:Bounds x="692" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_09fq7kh_di" bpmnElement="TextAnnotation_09fq7kh">
<dc:Bounds x="330" y="116" width="99.99202297383536" height="68.28334396936822" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1234e5n_di" bpmnElement="TextAnnotation_1234e5n">
<dc:Bounds x="570" y="120" width="100" height="68" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_10keafb_di" bpmnElement="Task_Get_Fact_From_API">
<dc:Bounds x="470" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1cfasjp_di" bpmnElement="Association_1cfasjp">
<di:waypoint x="344" y="210" />
<di:waypoint x="359" y="184" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1qirnyy_di" bpmnElement="Association_1qirnyy">
<di:waypoint x="561" y="210" />
<di:waypoint x="584" y="188" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,65 @@
"""example_data."""
import glob
import os
from flask import current_app
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
class ExampleDataLoader:
"""ExampleDataLoader."""
def create_spec(self, id, display_name="", description="", filepath=None, master_spec=False,
process_group_id='', display_order=0, from_tests=False, standalone=False, library=False):
"""Assumes that a directory exists in static/bpmn with the same name as the given id.
further assumes that the [id].bpmn is the primary file for the process model.
returns an array of data models to be added to the database.
"""
global file
spec = ProcessModelInfo(id=id,
display_name=display_name,
description=description,
process_group_id=process_group_id,
display_order=display_order,
is_master_spec=master_spec,
standalone=standalone,
library=library,
primary_file_name="",
primary_process_id="",
is_review=False,
libraries=[])
workflow_spec_service = ProcessModelService()
workflow_spec_service.add_spec(spec)
if not filepath and not from_tests:
filepath = os.path.join(current_app.root_path, 'static', 'bpmn', id, "*.*")
if not filepath and from_tests:
filepath = os.path.join(current_app.root_path, '..', 'tests', 'data', id, "*.*")
files = glob.glob(filepath)
for file_path in files:
if os.path.isdir(file_path):
continue # Don't try to process sub directories
noise, file_extension = os.path.splitext(file_path)
filename = os.path.basename(file_path)
is_primary = filename.lower() == id + '.bpmn'
file = None
try:
file = open(file_path, 'rb')
data = file.read()
SpecFileService.add_file(workflow_spec=spec, file_name=filename, binary_data=data)
if is_primary:
SpecFileService.set_primary_bpmn(spec, filename, data)
workflow_spec_service = ProcessModelService()
workflow_spec_service.update_spec(spec)
except IsADirectoryError:
# Ignore sub directories
pass
finally:
if file:
file.close()
return spec

View File

@ -1,27 +1,59 @@
"""User."""
from typing import Any
from flask_bpmn.models.db import db
from spiff_workflow_webapp.models.process_group import ProcessGroupModel
from spiff_workflow_webapp.models.user import UserModel
import os
def find_or_create_user(username: str = "test_user1") -> Any:
user = UserModel.query.filter_by(username=username).first()
if user is None:
user = UserModel(username=username)
db.session.add(user)
db.session.commit()
from spiff_workflow_webapp.models.process_group import ProcessGroup
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
return user
from tests.spiff_workflow_webapp.helpers.example_data import ExampleDataLoader
def find_or_create_process_group(name: str = "test_group1") -> Any:
process_group = ProcessGroupModel.query.filter_by(name=name).first()
# def find_or_create_user(username: str = "test_user1") -> Any:
# user = UserModel.query.filter_by(username=username).first()
# if user is None:
# user = UserModel(username=username)
# db.session.add(user)
# db.session.commit()
#
# return user
#
#
# def find_or_create_process_group(name: str = "test_group1") -> Any:
# process_group = ProcessGroupModel.query.filter_by(name=name).first()
# if process_group is None:
# process_group = ProcessGroupModel(name=name)
# db.session.add(process_group)
# db.session.commit()
#
# return process_group
def assure_process_group_exists(process_group_id=None):
"""Assure_process_group_exists."""
process_group = None
workflow_spec_service = ProcessModelService()
if process_group_id is not None:
process_group = workflow_spec_service.get_process_group(process_group_id)
if process_group is None:
process_group = ProcessGroupModel(name=name)
db.session.add(process_group)
db.session.commit()
process_group = ProcessGroup(id="test_process_group", display_name="Test Workflows", admin=False, display_order=0)
workflow_spec_service.add_process_group(process_group)
return process_group
def load_test_spec(app, dir_name, display_name=None, master_spec=False, process_group_id=None, library=False):
"""Loads a spec into the database based on a directory in /tests/data."""
process_group = None
workflow_spec_service = ProcessModelService()
if not master_spec and not library:
process_group = assure_process_group_exists(process_group_id)
process_group_id = process_group.id
workflow_spec = workflow_spec_service.get_spec(dir_name)
if workflow_spec:
return workflow_spec
else:
filepath = os.path.join(app.root_path, '..', 'tests', 'data', dir_name, "*")
if display_name is None:
display_name = dir_name
spec = ExampleDataLoader().create_spec(id=dir_name, filepath=filepath, master_spec=master_spec,
display_name=display_name, process_group_id=process_group_id, library=library)
return spec

View File

@ -2,35 +2,37 @@
import json
import pytest
import os
import io
import shutil
from typing import Union
from flask.testing import FlaskClient
from flask_bpmn.models.db import db
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
from spiff_workflow_webapp.models.process_model import ProcessModelInfoSchema, ProcessModelInfo
from spiff_workflow_webapp.models.process_group import ProcessGroup
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.models.file import FileType
from tests.spiff_workflow_webapp.helpers.test_data import load_test_spec
@pytest.fixture()
def process_group_resource():
def with_bpmn_file_cleanup():
"""Process_group_resource."""
print("setup")
process_model_service = ProcessModelService()
bpmn_root_path_test_cat = os.path.join(process_model_service.root_path(), "test_cat")
if os.path.exists(bpmn_root_path_test_cat):
shutil.rmtree(bpmn_root_path_test_cat)
if os.path.exists(process_model_service.root_path()):
shutil.rmtree(process_model_service.root_path())
yield "resource"
print("teardown")
if os.path.exists(bpmn_root_path_test_cat):
shutil.rmtree(bpmn_root_path_test_cat)
if os.path.exists(process_model_service.root_path()):
shutil.rmtree(process_model_service.root_path())
def test_add_new_process_modelification(client: FlaskClient, process_group_resource):
def test_add_new_process_modelification(client: FlaskClient, with_bpmn_file_cleanup):
"""Test_add_new_process_modelification."""
process_model_service = ProcessModelService()
assert(0 == len(process_model_service.get_specs()))
assert(0 == len(process_model_service.get_process_groups()))
@ -41,9 +43,9 @@ def test_add_new_process_modelification(client: FlaskClient, process_group_resou
standalone=False, is_review=False, is_master_spec=False, libraries=[], library=False,
primary_process_id='', primary_file_name='')
rv = client.post('/v1.0/workflow-specification',
# headers=logged_in_headers(),
content_type="application/json",
data=json.dumps(ProcessModelInfoSchema().dump(spec)))
# headers=logged_in_headers(),
content_type="application/json",
data=json.dumps(ProcessModelInfoSchema().dump(spec)))
assert rv.status_code == 200
fs_spec = process_model_service.get_spec('make_cookies')
@ -54,7 +56,7 @@ def test_add_new_process_modelification(client: FlaskClient, process_group_resou
# def test_get_process_modelification(self):
#
# load_test_spec('random_fact')
# rv = app.get('/v1.0/workflow-specification/random_fact', headers=logged_in_headers())
# rv = client.get('/v1.0/workflow-specification/random_fact', headers=logged_in_headers())
# assert_success(rv)
# json_data = json.loads(rv.get_data(as_text=True))
# api_spec = WorkflowSpecInfoSchema().load(json_data)
@ -62,3 +64,22 @@ def test_add_new_process_modelification(client: FlaskClient, process_group_resou
# fs_spec = process_model_service.get_spec('random_fact')
# assert(WorkflowSpecInfoSchema().dump(fs_spec) == json_data)
#
def test_create_spec_file(app, client: FlaskClient, with_bpmn_file_cleanup):
"""Test_create_spec_file."""
spec = load_test_spec(app, 'random_fact')
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
rv = client.post('/v1.0/workflow-specification/%s/file' % spec.id, data=data, follow_redirects=True,
content_type='multipart/form-data')
assert rv.status_code == 200
assert(rv.get_data() is not None)
file = json.loads(rv.get_data(as_text=True))
assert(FileType.svg.value == file['type'])
assert("image/svg+xml" == file['content_type'])
rv = client.get(f'/v1.0/workflow-specification/{spec.id}/file/random_fact.svg')
assert rv.status_code == 200
file2 = json.loads(rv.get_data(as_text=True))
assert(file == file2)