merged in main w/ burnettk cullerton jbirddog

This commit is contained in:
jasquat 2022-09-14 16:58:51 -04:00
commit 8f45f3d586
11 changed files with 209 additions and 82 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ node_modules
/tests/files/tickets.csv
/log/*.log
/tests/spiffworkflow_backend/files
/src/spiffworkflow_backend/config/secrets.py

21
poetry.lock generated
View File

@ -1709,9 +1709,9 @@ sphinx = ">=3.0"
unidecode = "*"
[package.extras]
go = ["sphinxcontrib-golangdomain"]
docs = ["sphinx", "sphinx-rtd-theme"]
dotnet = ["sphinxcontrib-dotnetdomain"]
docs = ["sphinx-rtd-theme", "sphinx"]
go = ["sphinxcontrib-golangdomain"]
[[package]]
name = "sphinx-autobuild"
@ -1847,7 +1847,7 @@ pytz = "*"
type = "git"
url = "https://github.com/sartography/SpiffWorkflow"
reference = "main"
resolved_reference = "0abc3c914d8c993abcf9fa02eab105e2a8cf6b71"
resolved_reference = "4d0976c98a9dc07d604c06fe0b7e3b21fb97c5b3"
[[package]]
name = "sqlalchemy"
@ -2341,7 +2341,10 @@ dparse = [
{file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"},
{file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"},
]
ecdsa = []
ecdsa = [
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
{file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"},
]
filelock = [
{file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
{file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
@ -2906,7 +2909,10 @@ python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
python-jose = []
python-jose = [
{file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"},
{file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"},
]
python-keycloak = []
pytz = [
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
@ -3103,7 +3109,10 @@ sphinx-basic-ng = [
{file = "sphinx_basic_ng-0.0.1a11-py3-none-any.whl", hash = "sha256:9aecb5345816998789ef76658a83e3c0a12aafa14b17d40e28cd4aaeb94d1517"},
{file = "sphinx_basic_ng-0.0.1a11.tar.gz", hash = "sha256:bf9a8fda0379c7d2ab51c9543f2b18e014b77fb295b49d64f3c1a910c863b34f"},
]
sphinx-click = []
sphinx-click = [
{file = "sphinx-click-4.3.0.tar.gz", hash = "sha256:bd4db5d3c1bec345f07af07b8e28a76cfc5006d997984e38ae246bbf8b9a3b38"},
{file = "sphinx_click-4.3.0-py3-none-any.whl", hash = "sha256:23e85a3cb0b728a421ea773699f6acadefae171d1a764a51dd8ec5981503ccbe"},
]
sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},

View File

@ -34,8 +34,8 @@ class ProcessModelInfo:
is_master_spec: bool | None = False
standalone: bool | None = False
library: bool | None = False
primary_file_name: str | None = ""
primary_process_id: str | None = ""
primary_file_name: str | None = None
primary_process_id: str | None = None
libraries: list[str] = field(default_factory=list)
display_order: int | None = 0
is_review: bool = False

View File

@ -243,7 +243,7 @@ def get_file(process_group_id: str, process_model_id: str, file_name: str) -> An
def process_model_file_update(
process_group_id: str, process_model_id: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_save."""
"""Process_model_file_update."""
process_model = get_process_model(process_model_id, process_group_id)
request_file = get_file_from_request()
@ -330,6 +330,9 @@ def process_instance_run(
if do_engine_steps:
try:
processor.do_engine_steps()
except ApiError as e:
ErrorHandlingService().handle_error(processor, e)
raise e
except Exception as e:
ErrorHandlingService().handle_error(processor, e)
task = processor.bpmn_process_instance.last_task
@ -773,7 +776,10 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
task.process_model_display_name = process_model.display_name
process_model_with_form = process_model
if task.process_name != process_model.primary_process_id:
all_processes = SpecFileService.get_all_bpmn_process_identifiers_for_process_model(
process_model
)
if task.process_name not in all_processes:
bpmn_file_full_path = (
ProcessInstanceProcessor.bpmn_file_full_path_from_bpmn_process_identifier(
task.process_name

View File

@ -72,7 +72,9 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer
from spiffworkflow_backend.services.spec_file_service import SpecFileService
from spiffworkflow_backend.services.user_service import UserService
# from crc.services.user_file_service import UserFileService
class ProcessInstanceProcessorError(Exception):
"""ProcessInstanceProcessorError."""
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
@ -97,11 +99,17 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
try:
return super()._evaluate(expression, context)
except Exception as exception:
raise WorkflowTaskExecException(
task,
"Error evaluating expression "
"'%s', %s" % (expression, str(exception)),
) from exception
if task is None:
raise ProcessInstanceProcessorError(
"Error evaluating expression: "
"'%s', exception: %s" % (expression, str(exception)),
) from exception
else:
raise WorkflowTaskExecException(
task,
"Error evaluating expression "
"'%s', %s" % (expression, str(exception)),
) from exception
def execute(self, task: SpiffTask, script: str) -> None:
"""Execute."""
@ -637,7 +645,7 @@ class ProcessInstanceProcessor:
if not bpmn_message.correlations:
raise ApiError(
"message_correlations_missing",
f"Could not find any message correlations bpmn_message: {bpmn_message}",
f"Could not find any message correlations bpmn_message: {bpmn_message.name}",
)
message_correlations = []

View File

@ -310,8 +310,6 @@ class ProcessModelService(FileSystemService):
is_master_spec=is_master,
display_name=name,
description="",
primary_process_id="",
primary_file_name="",
display_order=0,
is_review=False,
libraries=[],

View File

@ -24,6 +24,7 @@ from spiffworkflow_backend.models.message_triggerable_process_model import (
)
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
class SpecFileService(FileSystemService):
@ -193,12 +194,17 @@ class SpecFileService(FileSystemService):
try:
if set_primary_file:
process_model_info.primary_process_id = (
SpecFileService.get_bpmn_process_identifier(bpmn_etree_element)
)
process_model_info.primary_file_name = file_name
process_model_info.is_review = SpecFileService.has_swimlane(
bpmn_etree_element
attributes_to_update = {
"primary_process_id": (
SpecFileService.get_bpmn_process_identifier(
bpmn_etree_element
)
),
"primary_file_name": file_name,
"is_review": SpecFileService.has_swimlane(bpmn_etree_element),
}
ProcessModelService().update_spec(
process_model_info, attributes_to_update
)
SpecFileService.check_for_message_models(
@ -239,6 +245,54 @@ class SpecFileService(FileSystemService):
retval = True
return retval
@staticmethod
def append_identifier_of_process_to_array(
process_element: _Element, process_identifiers: list[str]
) -> None:
"""Append_identifier_of_process_to_array."""
process_id_key = "id"
if "name" in process_element.attrib:
process_id_key = "name"
process_identifiers.append(process_element.attrib[process_id_key])
@staticmethod
def get_all_bpmn_process_identifiers_for_process_model(
process_model_info: ProcessModelInfo,
) -> list[str]:
"""Get_all_bpmn_process_identifiers_for_process_model."""
if process_model_info.primary_file_name is None:
return []
binary_data = SpecFileService.get_data(
process_model_info, process_model_info.primary_file_name
)
et_root: EtreeElement = SpecFileService.get_etree_element_from_binary_data(
binary_data, process_model_info.primary_file_name
)
process_identifiers: list[str] = []
for child in et_root:
if child.tag.endswith("process") and child.attrib.get(
"isExecutable", False
):
subprocesses = child.xpath(
"//bpmn:subProcess",
namespaces={"bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL"},
)
for subprocess in subprocesses:
SpecFileService.append_identifier_of_process_to_array(
subprocess, process_identifiers
)
SpecFileService.append_identifier_of_process_to_array(
child, process_identifiers
)
if len(process_identifiers) == 0:
raise ValidationException("No executable process tag found")
return process_identifiers
@staticmethod
def get_executable_process_elements(et_root: _Element) -> list[_Element]:
"""Get_executable_process_elements."""

View File

@ -1,11 +1,13 @@
"""Base_test."""
import io
import json
import os
import time
from typing import Any
from typing import Dict
from typing import Optional
from flask import current_app
from flask.app import Flask
from flask.testing import FlaskClient
from flask_bpmn.api.api_error import ApiError
@ -78,14 +80,14 @@ class BaseTest:
assert response.status_code == 201
return response
def create_process_model(
def create_process_model_with_api(
self,
client: FlaskClient,
process_group_id: Optional[str] = None,
process_model_id: Optional[str] = None,
process_model_display_name: Optional[str] = None,
process_model_description: Optional[str] = None,
fault_or_suspend_on_exception: Optional[str] = None,
process_model_id: str = "make_cookies",
process_model_display_name: str = "Cooooookies",
process_model_description: str = "Om nom nom delicious cookies",
fault_or_suspend_on_exception: str = NotificationType.suspend.value,
exception_notification_addresses: Optional[list] = None,
primary_process_id: Optional[str] = None,
primary_file_name: Optional[str] = None,
@ -105,20 +107,8 @@ class BaseTest:
else:
process_group = ProcessModelService().get_process_group(process_group_id)
if process_model_id is None:
process_model_id = "make_cookies"
if process_model_display_name is None:
process_model_display_name = "Cooooookies"
if process_model_description is None:
process_model_description = "Om nom nom delicious cookies"
if fault_or_suspend_on_exception is None:
fault_or_suspend_on_exception = NotificationType.suspend.value
if exception_notification_addresses is None:
exception_notification_addresses = []
if primary_process_id is None:
primary_process_id = ""
if primary_file_name is None:
primary_file_name = ""
model = ProcessModelInfo(
id=process_model_id,
display_name=process_model_display_name,
@ -147,25 +137,21 @@ class BaseTest:
def create_spec_file(
self,
client: FlaskClient,
process_group_id: str = "",
process_model_id: str = "",
file_name: str = "",
file_data: bytes = b"",
process_group_id: str = "random_fact",
process_model_id: str = "random_fact",
process_model: Optional[ProcessModelInfo] = None,
file_name: str = "random_fact.svg",
file_data: bytes = b"abcdef",
) -> Any:
"""Test_create_spec_file."""
if process_group_id == "":
process_group_id = "random_fact"
if process_model_id == "":
process_model_id = "random_fact"
if file_name == "":
file_name = "random_fact.svg"
if file_data == b"":
file_data = b"abcdef"
spec = load_test_spec(process_model_id, process_group_id=process_group_id)
if process_model is None:
process_model = load_test_spec(
process_model_id, process_group_id=process_group_id
)
data = {"file": (io.BytesIO(file_data), file_name)}
user = self.find_or_create_user()
response = client.post(
f"/v1.0/process-models/{spec.process_group_id}/{spec.id}/files",
f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files",
data=data,
follow_redirects=True,
content_type="multipart/form-data",
@ -178,7 +164,7 @@ class BaseTest:
# assert "image/svg+xml" == file["content_type"]
response = client.get(
f"/v1.0/process-models/{spec.process_group_id}/{spec.id}/files/{file_name}",
f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/{file_name}",
headers=self.logged_in_headers(user),
)
assert response.status_code == 200
@ -260,3 +246,20 @@ class BaseTest:
# self.assertEqual(uid, user.uid, 'Logged in user should match given user uid')
return dict(Authorization="Bearer " + user.encode_auth_token())
def get_test_data_file_contents(
self, file_name: str, process_model_test_data_dir: str
) -> bytes:
"""Get_test_data_file_contents."""
current_app.root_path,
file_full_path = os.path.join(
current_app.root_path,
"..",
"..",
"tests",
"data",
process_model_test_data_dir,
file_name,
)
with open(file_full_path, "rb") as file:
return file.read()

View File

@ -41,8 +41,6 @@ class ExampleDataLoader:
is_master_spec=master_spec,
standalone=standalone,
library=library,
primary_file_name="",
primary_process_id="",
is_review=False,
libraries=[],
)

View File

@ -38,29 +38,82 @@ class TestProcessApi(BaseTest):
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_add_new_process_model."""
# group_id = None,
model_id = "make_cookies"
model_display_name = "Cooooookies"
model_description = "Om nom nom delicious cookies"
self.create_process_model(
process_model_identifier = "sample"
model_display_name = "Sample"
model_description = "The sample"
self.create_process_model_with_api(
client,
process_group_id=None,
process_model_id=model_id,
process_model_id=process_model_identifier,
process_model_display_name=model_display_name,
process_model_description=model_description,
)
process_model = ProcessModelService().get_process_model(model_id)
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
assert model_display_name == process_model.display_name
assert 0 == process_model.display_order
assert 1 == len(ProcessModelService().get_process_groups())
self.create_spec_file(client)
bpmn_file_name = "sample.bpmn"
bpmn_file_data_bytes = self.get_test_data_file_contents(
bpmn_file_name, "sample"
)
self.create_spec_file(
client,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
process_model=process_model,
)
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
assert process_model.primary_file_name == bpmn_file_name
assert process_model.primary_process_id == "sample"
def test_primary_process_id_updates_via_xml(
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_primary_process_id_updates_via_xml."""
process_model_identifier = "sample"
initial_primary_process_id = "sample"
terminal_primary_process_id = "new_process_id"
process_model = load_test_spec(process_model_id=process_model_identifier)
assert process_model.primary_process_id == initial_primary_process_id
bpmn_file_name = "sample.bpmn"
bpmn_file_data_bytes = self.get_test_data_file_contents(
bpmn_file_name, "sample"
)
bpmn_file_data_string = bpmn_file_data_bytes.decode("utf-8")
old_string = f'bpmn:process id="{initial_primary_process_id}"'
new_string = f'bpmn:process id="{terminal_primary_process_id}"'
updated_bpmn_file_data_string = bpmn_file_data_string.replace(
old_string, new_string
)
updated_bpmn_file_data_bytes = bytearray(updated_bpmn_file_data_string, "utf-8")
data = {"file": (io.BytesIO(updated_bpmn_file_data_bytes), bpmn_file_name)}
user = self.find_or_create_user()
response = client.put(
f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/{bpmn_file_name}",
data=data,
follow_redirects=True,
content_type="multipart/form-data",
headers=logged_in_headers(user),
)
assert response.status_code == 200
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
assert process_model.primary_file_name == bpmn_file_name
assert process_model.primary_process_id == terminal_primary_process_id
def test_process_model_delete(
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_process_model_delete."""
self.create_process_model(client)
self.create_process_model_with_api(client)
# assert we have a model
process_model = ProcessModelService().get_process_model("make_cookies")
@ -117,7 +170,7 @@ class TestProcessApi(BaseTest):
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_process_model_update."""
self.create_process_model(client)
self.create_process_model_with_api(client)
process_model = ProcessModelService().get_process_model("make_cookies")
assert process_model.id == "make_cookies"
assert process_model.display_name == "Cooooookies"
@ -149,11 +202,15 @@ class TestProcessApi(BaseTest):
# add 5 models to the group
for i in range(5):
model_id = f"test_model_{i}"
process_model_identifier = f"test_model_{i}"
model_display_name = f"Test Model {i}"
model_description = f"Test Model {i} Description"
self.create_process_model(
client, group_id, model_id, model_display_name, model_description
self.create_process_model_with_api(
client,
group_id,
process_model_identifier,
model_display_name,
model_description,
)
# get all models
@ -1231,16 +1288,11 @@ class TestProcessApi(BaseTest):
assert response.status_code == 400
api_error = json.loads(response.get_data(as_text=True))
assert api_error["code"] == "unknown_exception"
assert "An unknown error occurred." in api_error["message"]
assert api_error["code"] == "task_error"
assert (
'Original error: ApiError: Activity_CauseError: TypeError:can only concatenate str (not "int") to str.'
'Activity_CauseError: TypeError:can only concatenate str (not "int") to str'
in api_error["message"]
)
assert (
"Error in task 'Cause Error' (Activity_CauseError)." in api_error["message"]
)
assert "Error is on line 1. In file error.bpmn." in api_error["message"]
process = (
db.session.query(ProcessInstanceModel)

View File

@ -1,12 +1,10 @@
"""Test_secret_service."""
import json
from flask.app import Flask
from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.secret_model import SecretModel
from spiffworkflow_backend.models.secret_model import SecretModelSchema
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.secret_service import SecretService