Merge pull request #110 from sartography/feature/jinja_errors

Feature/jinja errors
This commit is contained in:
Kevin Burnett 2023-01-26 08:24:48 -08:00 committed by GitHub
commit acd26c0423
14 changed files with 399 additions and 62 deletions

View File

@ -47,7 +47,10 @@ if [[ "${SPIFFWORKFLOW_BACKEND_RUN_DATA_SETUP:-}" != "false" ]]; then
SPIFFWORKFLOW_BACKEND_FAIL_ON_INVALID_PROCESS_MODELS=false poetry run python bin/save_all_bpmn.py
fi
export IS_GUNICORN="true"
# Assure that the the Process Models Directory is initialized as a git repo
git init "${BPMN_SPEC_ABSOLUTE_DIR}"
git config --global --add safe.directory "${BPMN_SPEC_ABSOLUTE_DIR}"
export IS_GUNICORN="true"
# THIS MUST BE THE LAST COMMAND!
exec poetry run gunicorn ${additional_args} --bind "0.0.0.0:$port" --workers="$workers" --limit-request-line 8192 --timeout 90 --capture-output --access-logfile '-' --log-level debug wsgi:app

89
poetry.lock generated
View File

@ -72,7 +72,7 @@ zookeeper = ["kazoo"]
[[package]]
name = "astroid"
version = "2.12.12"
version = "2.13.3"
description = "An abstract syntax tree for Python with inference support."
category = "main"
optional = false
@ -80,7 +80,7 @@ python-versions = ">=3.7.2"
[package.dependencies]
lazy-object-proxy = ">=1.4.0"
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
wrapt = [
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
{version = ">=1.14,<2", markers = "python_version >= \"3.11\""},
@ -430,6 +430,17 @@ calendars = ["convertdate", "convertdate", "hijri-converter"]
fasttext = ["fasttext"]
langdetect = ["langdetect"]
[[package]]
name = "dill"
version = "0.3.6"
description = "serialize all of python"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "distlib"
version = "0.3.6"
@ -854,6 +865,20 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "isort"
version = "5.11.4"
description = "A Python utility / library to sort Python imports."
category = "main"
optional = false
python-versions = ">=3.7.0"
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "itsdangerous"
version = "2.1.2"
@ -1029,7 +1054,7 @@ tests = ["pytest", "pytest-lazy-fixture (>=0.6.2)"]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
@ -1136,7 +1161,7 @@ flake8 = ">=3.9.1"
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
category = "main"
optional = false
python-versions = ">=3.7"
@ -1266,6 +1291,32 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pylint"
version = "2.15.10"
description = "python code static checker"
category = "main"
optional = false
python-versions = ">=3.7.2"
[package.dependencies]
astroid = ">=2.12.13,<=2.14.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
{version = ">=0.3.6", markers = "python_version >= \"3.11\""},
]
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
tomlkit = ">=0.10.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]]
name = "pyparsing"
version = "3.0.9"
@ -1873,6 +1924,14 @@ category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "tomlkit"
version = "0.11.6"
description = "Style preserving TOML library"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "tornado"
version = "6.2"
@ -2145,7 +2204,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "1.1"
python-versions = ">=3.9,<3.12"
content-hash = "701115e291a4014376871a0004a8d27e14c4a9092bd8c07e4ca190dd374b221a"
content-hash = "95c08ed2de5b5d047474666c9e9a5ff3e7e94e6184649c2aa6d3a961711f14b0"
[metadata.files]
alabaster = [
@ -2169,8 +2228,8 @@ apscheduler = [
{file = "APScheduler-3.9.1.post1.tar.gz", hash = "sha256:b2bea0309569da53a7261bfa0ce19c67ddbfe151bda776a6a907579fdbd3eb2a"},
]
astroid = [
{file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"},
{file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"},
{file = "astroid-2.13.3-py3-none-any.whl", hash = "sha256:14c1603c41cc61aae731cad1884a073c4645e26f126d13ac8346113c95577f3b"},
{file = "astroid-2.13.3.tar.gz", hash = "sha256:6afc22718a48a689ca24a97981ad377ba7fb78c133f40335dfd16772f29bcfb1"},
]
attrs = [
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
@ -2354,6 +2413,10 @@ dateparser = [
{file = "dateparser-1.1.2-py2.py3-none-any.whl", hash = "sha256:d31659dc806a7d88e2b510b2c74f68b525ae531f145c62a57a99bd616b7f90cf"},
{file = "dateparser-1.1.2.tar.gz", hash = "sha256:3821bf191f95b2658c4abd91571c09821ce7a2bc179bf6cefd8b4515c3ccf9ef"},
]
dill = [
{file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"},
{file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"},
]
distlib = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
@ -2532,6 +2595,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
{file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
]
itsdangerous = [
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
@ -2925,6 +2992,10 @@ pyjwt = [
{file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
{file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
]
pylint = [
{file = "pylint-2.15.10-py3-none-any.whl", hash = "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e"},
{file = "pylint-2.15.10.tar.gz", hash = "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
@ -3356,6 +3427,10 @@ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
tomlkit = [
{file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
{file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
]
tornado = [
{file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"},
{file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"},

View File

@ -74,6 +74,7 @@ pytz = "^2022.6"
dateparser = "^1.1.2"
types-dateparser = "^1.1.4.1"
flask-jwt-extended = "^4.4.4"
pylint = "^2.15.10"
[tool.poetry.dev-dependencies]

View File

@ -28,17 +28,6 @@ groups:
users:
[
admin@spiffworkflow.org,
oskar@spiffworkflow.org
]
Education:
users:
[
malala@spiffworkflow.org
]
President:
users:
[
nelson@spiffworkflow.org
]
permissions:
@ -82,21 +71,6 @@ permissions:
users: [ ]
allowed_permissions: [ read ]
uri: /processes
# Members of the Education group can change the processes under "education".
education-admin:
groups: ["Education", "President"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /process-groups/education:*
# Anyone can start an education process.
education-everybody:
groups: [everybody]
users: []
allowed_permissions: [create, read]
uri: /process-instances/misc:category_number_one:process-model-with-form/*
# Anyone can see their own user groups.
groups-everybody:
groups: [everybody]
users: []

View File

@ -13,6 +13,8 @@
<div class="error">{{error_message}}</div>
<div class="login">
<form id="login" method="post" action="{{ url_for('openid.form_submit') }}">
<p><b>Important:</b> This login form is for demonstration purposes only. In production systems you should
be using a real Open ID System.</p>
<input type="text" class="cds--text-input" name="Uname" id="Uname" placeholder="Username">
<br><br>
<input type="Password" class="cds--text-input" name="Pass" id="Pass" placeholder="Password">

View File

@ -15,6 +15,8 @@ from flask import g
from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from jinja2 import TemplateSyntaxError
from SpiffWorkflow.exceptions import WorkflowTaskException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState
from sqlalchemy import and_
@ -32,6 +34,7 @@ from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.process_api_blueprint import (
_find_principal_or_raise,
@ -251,7 +254,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
form_contents = _prepare_form_data(
form_schema_file_name,
task.data,
spiff_task,
process_model_with_form,
)
@ -271,7 +274,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
) from exception
if task.data:
_update_form_schema_with_task_data_as_needed(form_dict, task.data)
_update_form_schema_with_task_data_as_needed(form_dict, task)
if form_contents:
task.form_schema = form_dict
@ -279,7 +282,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
if form_ui_schema_file_name:
ui_form_contents = _prepare_form_data(
form_ui_schema_file_name,
task.data,
task,
process_model_with_form,
)
if ui_form_contents:
@ -287,9 +290,15 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
if task.properties and task.data and "instructionsForEndUser" in task.properties:
if task.properties["instructionsForEndUser"]:
task.properties["instructionsForEndUser"] = _render_jinja_template(
task.properties["instructionsForEndUser"], task.data
)
try:
task.properties["instructionsForEndUser"] = _render_jinja_template(
task.properties["instructionsForEndUser"], spiff_task
)
except WorkflowTaskException as wfe:
wfe.add_note("Failed to render instructions for end user.")
raise ApiError.from_workflow_exception(
"instructions_error", str(wfe), exp=wfe
) from wfe
return make_response(jsonify(task), 200)
@ -501,23 +510,45 @@ def _get_tasks(
def _prepare_form_data(
form_file: str, task_data: Union[dict, None], process_model: ProcessModelInfo
form_file: str, spiff_task: SpiffTask, process_model: ProcessModelInfo
) -> str:
"""Prepare_form_data."""
if task_data is None:
if spiff_task.data is None:
return ""
file_contents = SpecFileService.get_data(process_model, form_file).decode("utf-8")
return _render_jinja_template(file_contents, task_data)
try:
return _render_jinja_template(file_contents, spiff_task)
except WorkflowTaskException as wfe:
wfe.add_note(f"Error in Json Form File '{form_file}'")
api_error = ApiError.from_workflow_exception(
"instructions_error", str(wfe), exp=wfe
)
api_error.file_name = form_file
raise api_error
def _render_jinja_template(unprocessed_template: str, data: dict[str, Any]) -> str:
def _render_jinja_template(unprocessed_template: str, spiff_task: SpiffTask) -> str:
"""Render_jinja_template."""
jinja_environment = jinja2.Environment(
autoescape=True, lstrip_blocks=True, trim_blocks=True
)
template = jinja_environment.from_string(unprocessed_template)
return template.render(**data)
try:
template = jinja_environment.from_string(unprocessed_template)
return template.render(**spiff_task.data)
except jinja2.exceptions.TemplateError as template_error:
wfe = WorkflowTaskException(
str(template_error), task=spiff_task, exception=template_error
)
if isinstance(template_error, TemplateSyntaxError):
wfe.line_number = template_error.lineno
wfe.error_line = template_error.source.split("\n")[
template_error.lineno - 1
]
wfe.add_note(
"Jinja2 template errors can happen when trying to displaying task data"
)
raise wfe from template_error
def _get_spiff_task_from_process_instance(
@ -543,10 +574,11 @@ def _get_spiff_task_from_process_instance(
# originally from: https://bitcoden.com/answers/python-nested-dictionary-update-value-where-any-nested-key-matches
def _update_form_schema_with_task_data_as_needed(
in_dict: dict, task_data: dict
) -> None:
def _update_form_schema_with_task_data_as_needed(in_dict: dict, task: Task) -> None:
"""Update_nested."""
if task.data is None:
return None
for k, value in in_dict.items():
if "anyOf" == k:
# value will look like the array on the right of "anyOf": ["options_from_task_data_var:awesome_options"]
@ -561,19 +593,25 @@ def _update_form_schema_with_task_data_as_needed(
"options_from_task_data_var:", ""
)
if task_data_var not in task_data:
if task_data_var not in task.data:
wte = WorkflowTaskException(
(
"Error building form. Attempting to create a"
" selection list with options from variable"
f" '{task_data_var}' but it doesn't exist in"
" the Task Data."
),
task=task,
)
raise (
ApiError(
ApiError.from_workflow_exception(
error_code="missing_task_data_var",
message=(
"Task data is missing variable:"
f" {task_data_var}"
),
status_code=500,
message=str(wte),
exp=wte,
)
)
select_options_from_task_data = task_data.get(task_data_var)
select_options_from_task_data = task.data.get(task_data_var)
if isinstance(select_options_from_task_data, list):
if all(
"value" in d and "label" in d
@ -596,11 +634,11 @@ def _update_form_schema_with_task_data_as_needed(
in_dict[k] = options_for_react_json_schema_form
elif isinstance(value, dict):
_update_form_schema_with_task_data_as_needed(value, task_data)
_update_form_schema_with_task_data_as_needed(value, task)
elif isinstance(value, list):
for o in value:
if isinstance(o, dict):
_update_form_schema_with_task_data_as_needed(o, task_data)
_update_form_schema_with_task_data_as_needed(o, task)
def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any:

View File

@ -873,7 +873,10 @@ class ProcessInstanceProcessor:
f"Event of type {event_definition.event_type} sent to process instance"
f" {self.process_instance_model.id}"
)
self.bpmn_process_instance.catch(event_definition)
try:
self.bpmn_process_instance.catch(event_definition)
except Exception as e:
print(e)
self.do_engine_steps(save=True)
def add_step(self, step: Union[dict, None] = None) -> None:

View File

@ -0,0 +1,45 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
<bpmn:process id="Proccess_With_Bad_Form" name="Process With Form" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0smvjir</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0smvjir" sourceRef="StartEvent_1" targetRef="Activity_1cscoeg" />
<bpmn:endEvent id="Event_00xci7j">
<bpmn:incoming>Flow_1boyhcj</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1boyhcj" sourceRef="Activity_1cscoeg" targetRef="Event_00xci7j" />
<bpmn:manualTask id="Activity_1cscoeg" name="DisplayInfo">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Hello {{ name }}
Department: {{ department }}
{{ x +=- 1}}
</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0smvjir</bpmn:incoming>
<bpmn:outgoing>Flow_1boyhcj</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Proccess_With_Bad_Form">
<bpmndi:BPMNEdge id="Flow_1boyhcj_di" bpmnElement="Flow_1boyhcj">
<di:waypoint x="340" y="117" />
<di:waypoint x="382" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0smvjir_di" bpmnElement="Flow_0smvjir">
<di:waypoint x="215" y="117" />
<di:waypoint x="240" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_00g930h_di" bpmnElement="Activity_1cscoeg">
<dc:Bounds x="240" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_00xci7j_di" bpmnElement="Event_00xci7j">
<dc:Bounds x="382" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,18 @@
{
"title": "Simple form",
"description": "A simple form example with some bad Jinja2 Syntax in it {{ x +=- 1}}",
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"title": "Name",
"default": "World"
},
"department": {
"type": "string",
"title": "Department",
"enum": ["Finance", "HR", "IT"]
}
}
}

View File

@ -0,0 +1,11 @@
{
"name": {
"ui:title": "Name",
"ui:description": "(Your name)"
},
"department": {
"ui:title": "Department",
"ui:description": "(Your department)"
},
"ui:order": ["name", "department"]
}

View File

@ -0,0 +1,63 @@
<?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:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Proccess_With_Bad_Form" name="Process With Form" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0smvjir</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0smvjir" sourceRef="StartEvent_1" targetRef="Activity_SimpleForm" />
<bpmn:endEvent id="Event_00xci7j">
<bpmn:incoming>Flow_1boyhcj</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1boyhcj" sourceRef="Activity_1cscoeg" targetRef="Event_00xci7j" />
<bpmn:manualTask id="Activity_1cscoeg" name="DisplayInfo">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Hello {{ name }}
Department: {{ department }}
</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1ly1khd</bpmn:incoming>
<bpmn:outgoing>Flow_1boyhcj</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_1ly1khd" sourceRef="Activity_SimpleForm" targetRef="Activity_1cscoeg" />
<bpmn:userTask id="Activity_SimpleForm" name="Simple Form">
<bpmn:extensionElements>
<spiffworkflow:properties>
<spiffworkflow:property name="formJsonSchemaFilename" value="simple_form.json" />
<spiffworkflow:property name="formUiSchemaFilename" value="simple_form_ui.json" />
</spiffworkflow:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0smvjir</bpmn:incoming>
<bpmn:outgoing>Flow_1ly1khd</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Proccess_WithForm">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_00xci7j_di" bpmnElement="Event_00xci7j">
<dc:Bounds x="592" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_00g930h_di" bpmnElement="Activity_1cscoeg">
<dc:Bounds x="430" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0x5k4l1_di" bpmnElement="Activity_SimpleForm">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0smvjir_di" bpmnElement="Flow_0smvjir">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1boyhcj_di" bpmnElement="Flow_1boyhcj">
<di:waypoint x="530" y="177" />
<di:waypoint x="592" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ly1khd_di" bpmnElement="Flow_1ly1khd">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -42,7 +42,7 @@ def load_test_spec(
) -> ProcessModelInfo:
"""Loads a bpmn file into the process model dir based on a directory in tests/data."""
if process_model_source_directory is None:
raise Exception("You must inclode a `process_model_source_directory`.")
raise Exception("You must include a `process_model_source_directory`.")
spec = ExampleDataLoader.create_spec(
process_model_id=process_model_id,

View File

@ -0,0 +1,103 @@
"""Test_various_bpmn_constructs."""
from typing import Any
from flask.app import Flask
from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend import db
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.user import UserModel
class TestForGoodErrors(BaseTest):
"""Assure when certain errors happen when rendering a jinaj2 error that it makes some sense."""
def get_next_user_task(
self,
process_instance_id: int,
client: FlaskClient,
with_super_admin_user: UserModel,
) -> Any:
"""Returns the next available user task for a given process instance, if possible."""
human_tasks = (
db.session.query(HumanTaskModel)
.filter(HumanTaskModel.process_instance_id == process_instance_id)
.all()
)
assert len(human_tasks) > 0, "No human tasks found for process."
human_task = human_tasks[0]
response = client.get(
f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
return response
def test_invalid_form(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""React json form schema with bad jinja syntax provides good error."""
process_model = load_test_spec(
process_model_id="group/simple_form_with_error",
process_model_source_directory="simple_form_with_error",
)
response = self.create_process_instance_from_process_model_id_with_api(
client,
# process_model.process_group_id,
process_model.id,
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
response = self.get_next_user_task(
process_instance_id, client, with_super_admin_user
)
assert response.json is not None
assert response.json["error_type"] == "TemplateSyntaxError"
assert response.json["line_number"] == 3
assert response.json["file_name"] == "simple_form.json"
def test_jinja2_error_message_for_end_user_instructions(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_task_data_is_set_even_if_process_instance_errors."""
process_model = load_test_spec(
process_model_id="group/end_user_instructions_error",
bpmn_file_name="instructions_error.bpmn",
process_model_source_directory="error",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=with_super_admin_user
)
response = client.post(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance.id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
response = self.get_next_user_task(
process_instance.id, client, with_super_admin_user
)
assert response.status_code == 400
assert response.json is not None
assert response.json["error_type"] == "TemplateSyntaxError"
assert response.json["line_number"] == 3
assert response.json["error_line"] == "{{ x +=- 1}}"
assert response.json["file_name"] == "instructions_error.bpmn"
assert "instructions for end user" in response.json["message"]
assert "Jinja2" in response.json["message"]
assert "unexpected '='" in response.json["message"]

View File

@ -1680,6 +1680,7 @@ class TestProcessApi(BaseTest):
f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert (
response.json["form_schema"]["definitions"]["Color"]["anyOf"][1]["title"]
@ -2809,7 +2810,7 @@ class TestProcessApi(BaseTest):
)
data = {
"dateTime": "timedelta(hours=1)",
"dateTime": "PT1H",
"external": True,
"internal": True,
"label": "Event_0e4owa3",