Merge remote-tracking branch 'origin/main' into feature/jinja_errors
This commit is contained in:
commit
a38ecc2e60
14
.flake8
14
.flake8
|
@ -8,11 +8,19 @@ rst-roles = class,const,func,meth,mod,ref
|
|||
rst-directives = deprecated
|
||||
|
||||
per-file-ignores =
|
||||
# More specific globs seem to overwrite the more generic ones so we have
|
||||
# to split them out by directory
|
||||
# So if you have a rule like:
|
||||
# tests/*: D102,D103
|
||||
# and a rule like:
|
||||
# tests/test_hey.py: D102
|
||||
# THEN, test_hey.py will NOT be excluding D103
|
||||
|
||||
# asserts are ok in tests
|
||||
spiffworkflow-backend/tests/*:S101
|
||||
spiffworkflow-backend/tests/*:S101,D102,D103,D101
|
||||
|
||||
# prefer naming functions descriptively rather than forcing comments
|
||||
spiffworkflow-backend/*:D102,D103
|
||||
spiffworkflow-backend/src/*:D102,D103,D101
|
||||
|
||||
spiffworkflow-backend/bin/keycloak_test_server.py:B950,D
|
||||
spiffworkflow-backend/conftest.py:S105
|
||||
|
@ -34,4 +42,4 @@ per-file-ignores =
|
|||
# TODO: fix the S issues:
|
||||
# S607 Starting a process with a partial executable path
|
||||
# S605 Starting a process with a shell: Seems safe, but may be changed in the future, consider rewriting without shell
|
||||
spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,D103,S605
|
||||
spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,S605,D102,D103,D101
|
||||
|
|
|
@ -67,7 +67,7 @@ jobs:
|
|||
path: sample-process-models
|
||||
- name: start_keycloak
|
||||
working-directory: ./spiffworkflow-backend
|
||||
run: ./keycloak/bin/start_keycloak 5
|
||||
run: ./keycloak/bin/start_keycloak
|
||||
- name: start_backend
|
||||
working-directory: ./spiffworkflow-backend
|
||||
run: ./bin/build_and_run_with_docker_compose
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
spiffworkflow-db:
|
||||
container_name: spiffworkflow-db
|
||||
image: mysql:8.0.29
|
||||
platform: linux/amd64
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
restart: "no"
|
||||
spiffworkflow-frontend:
|
||||
container_name: spiffworkflow-frontend
|
||||
image: ghcr.io/sartography/spiffworkflow-frontend:latest
|
||||
depends_on:
|
||||
spiffworkflow-backend:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- MYSQL_DATABASE=spiffworkflow_backend_development
|
||||
- MYSQL_ROOT_PASSWORD=my-secret-pw
|
||||
- MYSQL_TCP_PORT=8003
|
||||
APPLICATION_ROOT: "/"
|
||||
PORT0: "${SPIFF_FRONTEND_PORT:-8001}"
|
||||
ports:
|
||||
- "8003"
|
||||
healthcheck:
|
||||
test: mysql --user=root --password=my-secret-pw -e 'select 1' spiffworkflow_backend_development
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
- "${SPIFF_FRONTEND_PORT:-8001}:${SPIFF_FRONTEND_PORT:-8001}/tcp"
|
||||
|
||||
spiffworkflow-backend:
|
||||
# container_name: spiffworkflow-backend
|
||||
|
@ -28,58 +21,69 @@ services:
|
|||
spiffworkflow-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- APPLICATION_ROOT=/
|
||||
- SPIFFWORKFLOW_BACKEND_ENV=development
|
||||
- FLASK_DEBUG=0
|
||||
- FLASK_SESSION_SECRET_KEY=super_secret_key
|
||||
- OPEN_ID_SERVER_URL=http://localhost:8000/openid
|
||||
- SPIFFWORKFLOW_FRONTEND_URL=http://localhost:8001
|
||||
- SPIFFWORKFLOW_BACKEND_URL=http://localhost:8000
|
||||
- SPIFFWORKFLOW_BACKEND_PORT=8000
|
||||
- SPIFFWORKFLOW_BACKEND_UPGRADE_DB=true
|
||||
- SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqlconnector://root:my-secret-pw@spiffworkflow-db:8003/spiffworkflow_backend_development
|
||||
- BPMN_SPEC_ABSOLUTE_DIR=/app/process_models
|
||||
- SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=false
|
||||
- SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=example.yml
|
||||
- RUN_BACKGROUND_SCHEDULER=true
|
||||
- OPEN_ID_CLIENT_ID=spiffworkflow-backend
|
||||
- OPEN_ID_CLIENT_SECRET_KEY=my_open_id_secret_key
|
||||
APPLICATION_ROOT: "/"
|
||||
SPIFFWORKFLOW_BACKEND_ENV: "development"
|
||||
FLASK_DEBUG: "0"
|
||||
FLASK_SESSION_SECRET_KEY: "${FLASK_SESSION_SECRET_KEY:-super_secret_key}"
|
||||
OPEN_ID_SERVER_URL: "http://localhost:${SPIFF_BACKEND_PORT:-8000}/openid"
|
||||
SPIFFWORKFLOW_FRONTEND_URL: "http://localhost:${SPIFF_FRONTEND_PORT:-8001}"
|
||||
# WARNING: Frontend is a static site which assumes frontend port - 1 on localhost.
|
||||
SPIFFWORKFLOW_BACKEND_URL: "http://localhost:${SPIFF_BACKEND_PORT:-8000}"
|
||||
SPIFFWORKFLOW_BACKEND_PORT: "${SPIFF_BACKEND_PORT:-8000}"
|
||||
SPIFFWORKFLOW_BACKEND_UPGRADE_DB: "true"
|
||||
SPIFFWORKFLOW_BACKEND_DATABASE_URI: "mysql+mysqlconnector://root:${SPIFF_MYSQL_PASS:-my-secret-pw}@spiffworkflow-db:${SPIFF_MYSQL_PORT:-8003}/spiffworkflow_backend_development"
|
||||
BPMN_SPEC_ABSOLUTE_DIR: "/app/process_models"
|
||||
SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA: "false"
|
||||
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME: "example.yml"
|
||||
RUN_BACKGROUND_SCHEDULER: "true"
|
||||
OPEN_ID_CLIENT_ID: "spiffworkflow-backend"
|
||||
OPEN_ID_CLIENT_SECRET_KEY: "my_open_id_secret_key"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "${SPIFF_BACKEND_PORT:-8000}:${SPIFF_BACKEND_PORT:-8000}/tcp"
|
||||
volumes:
|
||||
- ./process_models:/app/process_models
|
||||
- ./log:/app/log
|
||||
healthcheck:
|
||||
test: curl localhost:8000/v1.0/status --fail
|
||||
test: "curl localhost:${SPIFF_BACKEND_PORT:-8000}/v1.0/status --fail"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
spiffworkflow-frontend:
|
||||
container_name: spiffworkflow-frontend
|
||||
image: ghcr.io/sartography/spiffworkflow-frontend
|
||||
environment:
|
||||
- APPLICATION_ROOT=/
|
||||
- PORT0=8001
|
||||
ports:
|
||||
- "8001:8001"
|
||||
|
||||
spiffworkflow-connector:
|
||||
container_name: spiffworkflow-connector
|
||||
image: ghcr.io/sartography/connector-proxy-demo
|
||||
image: ghcr.io/sartography/connector-proxy-demo:latest
|
||||
environment:
|
||||
- FLASK_ENV=${FLASK_ENV:-development}
|
||||
- FLASK_DEBUG=0
|
||||
- FLASK_SESSION_SECRET_KEY=${FLASK_SESSION_SECRET_KEY:-super_secret_key}
|
||||
- CONNECTOR_PROXY_PORT=8004
|
||||
FLASK_ENV: "${FLASK_ENV:-development}"
|
||||
FLASK_DEBUG: "0"
|
||||
FLASK_SESSION_SECRET_KEY: "${FLASK_SESSION_SECRET_KEY:-super_secret_key}"
|
||||
CONNECTOR_PROXY_PORT: "${SPIFF_CONNECTOR_PORT:-8004}"
|
||||
ports:
|
||||
- "8004:8004"
|
||||
- "${SPIFF_CONNECTOR_PORT:-8004}:${SPIFF_CONNECTOR_PORT:-8004}/tcp"
|
||||
healthcheck:
|
||||
test: curl localhost:8004/liveness --fail
|
||||
test: "curl localhost:${SPIFF_CONNECTOR_PORT:-8004}/liveness --fail"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
spiffworkflow-db:
|
||||
container_name: spiffworkflow-db
|
||||
image: mysql:8.0.29
|
||||
platform: linux/amd64
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
restart: "no"
|
||||
environment:
|
||||
MYSQL_DATABASE: "spiffworkflow_backend_development"
|
||||
MYSQL_ROOT_PASSWORD: "${SPIFF_MYSQL_PASS:-my-secret-pw}"
|
||||
MYSQL_TCP_PORT: "${SPIFF_MYSQL_PORT:-8003}"
|
||||
ports:
|
||||
- "${SPIFF_MYSQL_PORT:-8003}:${SPIFF_MYSQL_PORT:-8003}/tcp"
|
||||
healthcheck:
|
||||
test: "mysql --user=root --password=${SPIFF_MYSQL_PASS:-my-secret-pw} -e 'select 1' spiffworkflow_backend_development"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
|
||||
volumes:
|
||||
spiffworkflow_backend:
|
||||
|
|
|
@ -8,11 +8,19 @@ rst-roles = class,const,func,meth,mod,ref
|
|||
rst-directives = deprecated
|
||||
|
||||
per-file-ignores =
|
||||
# More specific globs seem to overwrite the more generic ones so we have
|
||||
# to split them out by directory
|
||||
# So if you have a rule like:
|
||||
# tests/*: D102,D103
|
||||
# and a rule like:
|
||||
# tests/test_hey.py: D102
|
||||
# THEN, test_hey.py will NOT be excluding D103
|
||||
|
||||
# asserts are ok in tests
|
||||
tests/*:S101
|
||||
tests/*:S101,D102,D103
|
||||
|
||||
# prefer naming functions descriptively rather than forcing comments
|
||||
*:D102
|
||||
src/*:D102,D103
|
||||
|
||||
bin/keycloak_test_server.py:B950,D
|
||||
conftest.py:S105
|
||||
|
@ -31,4 +39,4 @@ per-file-ignores =
|
|||
# and ignore long comment line
|
||||
src/spiffworkflow_backend/services/logging_service.py:N802,B950
|
||||
|
||||
tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,D103,S605
|
||||
tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,S605,D102,D103,D101
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
"""Get the bpmn process json for a given process instance id and store it in /tmp."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from spiffworkflow_backend import create_app
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.secret_service import SecretService
|
||||
|
||||
|
||||
def main(env_file: str):
|
||||
"""Main."""
|
||||
os.environ["SPIFFWORKFLOW_BACKEND_ENV"] = "development"
|
||||
if os.environ.get("BPMN_SPEC_ABSOLUTE_DIR") is None:
|
||||
os.environ["BPMN_SPEC_ABSOLUTE_DIR"] = "hey"
|
||||
flask_env_key = "FLASK_SESSION_SECRET_KEY"
|
||||
os.environ[flask_env_key] = "whatevs"
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
contents = None
|
||||
with open(env_file, 'r') as f:
|
||||
contents = f.readlines()
|
||||
for line in contents:
|
||||
key, value_raw = line.split('=')
|
||||
value = value_raw.replace('"', '').rstrip()
|
||||
SecretService().add_secret(key, value, UserModel.query.first().id)
|
||||
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
raise Exception("env file must be specified")
|
||||
|
||||
main(sys.argv[1])
|
|
@ -1,8 +1,8 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 22212a7d6505
|
||||
Revision ID: 2ec4222f0012
|
||||
Revises:
|
||||
Create Date: 2023-01-23 10:59:17.365694
|
||||
Create Date: 2023-01-24 10:31:26.693063
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '22212a7d6505'
|
||||
revision = '2ec4222f0012'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
@ -129,6 +129,8 @@ def upgrade():
|
|||
sa.Column('bpmn_version_control_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('bpmn_version_control_identifier', sa.String(length=255), nullable=True),
|
||||
sa.Column('spiff_step', sa.Integer(), nullable=True),
|
||||
sa.Column('locked_by', sa.String(length=80), nullable=True),
|
||||
sa.Column('locked_at_in_seconds', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
@ -204,8 +206,7 @@ def upgrade():
|
|||
sa.ForeignKeyConstraint(['completed_by_user_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ),
|
||||
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('task_id', 'process_instance_id', name='human_task_unique')
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_human_task_completed'), 'human_task', ['completed'], unique=False)
|
||||
op.create_table('message_correlation',
|
|
@ -72,7 +72,7 @@ zookeeper = ["kazoo"]
|
|||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "2.13.2"
|
||||
version = "2.12.12"
|
||||
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 = ">=4.0.0"
|
||||
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
|
||||
wrapt = [
|
||||
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
|
||||
{version = ">=1.14,<2", markers = "python_version >= \"3.11\""},
|
||||
|
@ -430,17 +430,6 @@ 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"
|
||||
|
@ -878,20 +867,6 @@ 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"
|
||||
|
@ -1067,7 +1042,7 @@ tests = ["pytest", "pytest-lazy-fixture (>=0.6.2)"]
|
|||
name = "mccabe"
|
||||
version = "0.6.1"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
|
@ -1174,7 +1149,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 = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
|
@ -1304,32 +1279,6 @@ 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"
|
||||
|
@ -1837,8 +1786,8 @@ lxml = "*"
|
|||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/sartography/SpiffWorkflow"
|
||||
reference = "main"
|
||||
resolved_reference = "7378639d349ed61d907a6891740760e5eee20d1a"
|
||||
reference = "450ef3bcd639b6bc1c115fbe35bf3f93946cb0c7"
|
||||
resolved_reference = "450ef3bcd639b6bc1c115fbe35bf3f93946cb0c7"
|
||||
|
||||
[[package]]
|
||||
name = "SQLAlchemy"
|
||||
|
@ -1937,14 +1886,6 @@ 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"
|
||||
|
@ -2217,7 +2158,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.9,<3.12"
|
||||
content-hash = "681d047a32e12d71a39ff9de8cd6ceffc41048127243f427b69245d5e071419d"
|
||||
content-hash = "ba797b1ccf2dd8dc50d62ff06f6667f28e241b0a26611192d53abfc75b29a415"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
|
@ -2241,8 +2182,8 @@ apscheduler = [
|
|||
{file = "APScheduler-3.9.1.post1.tar.gz", hash = "sha256:b2bea0309569da53a7261bfa0ce19c67ddbfe151bda776a6a907579fdbd3eb2a"},
|
||||
]
|
||||
astroid = [
|
||||
{file = "astroid-2.13.2-py3-none-any.whl", hash = "sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"},
|
||||
{file = "astroid-2.13.2.tar.gz", hash = "sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303"},
|
||||
{file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"},
|
||||
{file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||
|
@ -2426,10 +2367,6 @@ 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"},
|
||||
|
@ -2562,7 +2499,6 @@ greenlet = [
|
|||
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"},
|
||||
|
@ -2571,7 +2507,6 @@ greenlet = [
|
|||
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"},
|
||||
|
@ -2580,7 +2515,6 @@ greenlet = [
|
|||
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"},
|
||||
|
@ -2614,10 +2548,6 @@ 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"},
|
||||
|
@ -2882,7 +2812,10 @@ orjson = [
|
|||
{file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b68a42a31f8429728183c21fb440c21de1b62e5378d0d73f280e2d894ef8942e"},
|
||||
{file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff13410ddbdda5d4197a4a4c09969cb78c722a67550f0a63c02c07aadc624833"},
|
||||
{file = "orjson-3.8.0-cp310-none-win_amd64.whl", hash = "sha256:2d81e6e56bbea44be0222fb53f7b255b4e7426290516771592738ca01dbd053b"},
|
||||
{file = "orjson-3.8.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:200eae21c33f1f8b02a11f5d88d76950cd6fd986d88f1afe497a8ae2627c49aa"},
|
||||
{file = "orjson-3.8.0-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9529990f3eab54b976d327360aa1ff244a4b12cb5e4c5b3712fcdd96e8fe56d4"},
|
||||
{file = "orjson-3.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e2defd9527651ad39ec20ae03c812adf47ef7662bdd6bc07dabb10888d70dc62"},
|
||||
{file = "orjson-3.8.0-cp311-none-win_amd64.whl", hash = "sha256:b21c7af0ff6228ca7105f54f0800636eb49201133e15ddb80ac20c1ce973ef07"},
|
||||
{file = "orjson-3.8.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9e6ac22cec72d5b39035b566e4b86c74b84866f12b5b0b6541506a080fb67d6d"},
|
||||
{file = "orjson-3.8.0-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e2f4a5542f50e3d336a18cb224fc757245ca66b1fd0b70b5dd4471b8ff5f2b0e"},
|
||||
{file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1418feeb8b698b9224b1f024555895169d481604d5d884498c1838d7412794c"},
|
||||
|
@ -3008,10 +2941,6 @@ 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"},
|
||||
|
@ -3443,10 +3372,6 @@ 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"},
|
||||
|
|
|
@ -28,7 +28,8 @@ flask-migrate = "*"
|
|||
flask-restful = "*"
|
||||
werkzeug = "*"
|
||||
# temporarily switch off main to fix CI because poetry export doesn't capture the revision if it's not here (it ignores the lock)
|
||||
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"}
|
||||
# SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"}
|
||||
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "450ef3bcd639b6bc1c115fbe35bf3f93946cb0c7"}
|
||||
# SpiffWorkflow = {develop = true, path = "../SpiffWorkflow" }
|
||||
sentry-sdk = "^1.10"
|
||||
sphinx-autoapi = "^2.0"
|
||||
|
|
|
@ -19,7 +19,6 @@ from spiffworkflow_backend.exceptions.api_error import api_error_blueprint
|
|||
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
|
||||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.db import migrate
|
||||
from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint
|
||||
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import (
|
||||
openid_blueprint,
|
||||
)
|
||||
|
@ -106,7 +105,6 @@ def create_app() -> flask.app.Flask:
|
|||
|
||||
app.register_blueprint(user_blueprint)
|
||||
app.register_blueprint(api_error_blueprint)
|
||||
app.register_blueprint(admin_blueprint, url_prefix="/admin")
|
||||
app.register_blueprint(openid_blueprint, url_prefix="/openid")
|
||||
|
||||
# preflight options requests will be allowed if they meet the requirements of the url regex.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""__init__.py."""
|
||||
import os
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
from flask.app import Flask
|
||||
from werkzeug.utils import ImportStringError
|
||||
|
@ -96,6 +97,8 @@ def setup_config(app: Flask) -> None:
|
|||
if app.config["BPMN_SPEC_ABSOLUTE_DIR"] is None:
|
||||
raise ConfigurationError("BPMN_SPEC_ABSOLUTE_DIR config must be set")
|
||||
|
||||
app.config["PROCESS_UUID"] = uuid.uuid4()
|
||||
|
||||
setup_database_uri(app)
|
||||
setup_logger(app)
|
||||
|
||||
|
|
|
@ -82,3 +82,7 @@ SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID = environ.get(
|
|||
"SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID",
|
||||
default="Message_SystemMessageNotification",
|
||||
)
|
||||
|
||||
ALLOW_CONFISCATING_LOCK_AFTER_SECONDS = int(
|
||||
environ.get("ALLOW_CONFISCATING_LOCK_AFTER_SECONDS", default="600")
|
||||
)
|
||||
|
|
|
@ -26,9 +26,6 @@ class HumanTaskModel(SpiffworkflowBaseDBModel):
|
|||
"""HumanTaskModel."""
|
||||
|
||||
__tablename__ = "human_task"
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint("task_id", "process_instance_id", name="human_task_unique"),
|
||||
)
|
||||
|
||||
id: int = db.Column(db.Integer, primary_key=True)
|
||||
process_instance_id: int = db.Column(
|
||||
|
|
|
@ -75,6 +75,10 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||
) # type: ignore
|
||||
message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore
|
||||
message_correlations = relationship("MessageCorrelationModel", cascade="delete") # type: ignore
|
||||
process_metadata = relationship(
|
||||
"ProcessInstanceMetadataModel",
|
||||
cascade="delete",
|
||||
) # type: ignore
|
||||
|
||||
bpmn_json: str | None = deferred(db.Column(db.JSON)) # type: ignore
|
||||
start_in_seconds: int | None = db.Column(db.Integer)
|
||||
|
@ -83,11 +87,16 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||
created_at_in_seconds: int = db.Column(db.Integer)
|
||||
status: str = db.Column(db.String(50))
|
||||
|
||||
bpmn_xml_file_contents: str | None = None
|
||||
bpmn_version_control_type: str = db.Column(db.String(50))
|
||||
bpmn_version_control_identifier: str = db.Column(db.String(255))
|
||||
spiff_step: int = db.Column(db.Integer)
|
||||
|
||||
locked_by: str | None = db.Column(db.String(80))
|
||||
locked_at_in_seconds: int | None = db.Column(db.Integer)
|
||||
|
||||
bpmn_xml_file_contents: str | None = None
|
||||
process_model_with_diagram_identifier: str | None = None
|
||||
|
||||
@property
|
||||
def serialized(self) -> dict[str, Any]:
|
||||
"""Return object data in serializeable format."""
|
||||
|
@ -108,6 +117,14 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||
"process_initiator_username": self.process_initiator.username,
|
||||
}
|
||||
|
||||
def serialized_with_metadata(self) -> dict[str, Any]:
|
||||
process_instance_attributes = self.serialized
|
||||
process_instance_attributes["process_metadata"] = self.process_metadata
|
||||
process_instance_attributes["process_model_with_diagram_identifier"] = (
|
||||
self.process_model_with_diagram_identifier
|
||||
)
|
||||
return process_instance_attributes
|
||||
|
||||
@property
|
||||
def serialized_flat(self) -> dict:
|
||||
"""Return object in serializeable format with data merged together with top-level attributes.
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
"""__init__."""
|
|
@ -1,187 +0,0 @@
|
|||
"""APIs for dealing with process groups, process models, and process instances."""
|
||||
from typing import Union
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import flash
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceProcessor,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_instance_service import (
|
||||
ProcessInstanceService,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
||||
from spiffworkflow_backend.services.user_service import UserService
|
||||
|
||||
admin_blueprint = Blueprint(
|
||||
"admin", __name__, template_folder="templates", static_folder="static"
|
||||
)
|
||||
|
||||
ALLOWED_BPMN_EXTENSIONS = {"bpmn", "dmn"}
|
||||
|
||||
|
||||
@admin_blueprint.route("/process-groups", methods=["GET"])
|
||||
def process_group_list() -> str:
|
||||
"""Process_group_list."""
|
||||
process_groups = ProcessModelService.get_process_groups()
|
||||
return render_template("process_group_list.html", process_groups=process_groups)
|
||||
|
||||
|
||||
@admin_blueprint.route("/process-groups/<process_group_id>", methods=["GET"])
|
||||
def process_group_show(process_group_id: str) -> str:
|
||||
"""Show_process_group."""
|
||||
process_group = ProcessModelService.get_process_group(process_group_id)
|
||||
return render_template("process_group_show.html", process_group=process_group)
|
||||
|
||||
|
||||
@admin_blueprint.route("/process-models/<process_model_id>", methods=["GET"])
|
||||
def process_model_show(process_model_id: str) -> Union[str, Response]:
|
||||
"""Show_process_model."""
|
||||
process_model = ProcessModelService.get_process_model(process_model_id)
|
||||
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
|
||||
current_file_name = process_model.primary_file_name
|
||||
if current_file_name is None:
|
||||
flash("No primary_file_name", "error")
|
||||
return redirect(url_for("admin.process_group_list"))
|
||||
bpmn_xml = SpecFileService.get_data(process_model, current_file_name)
|
||||
return render_template(
|
||||
"process_model_show.html",
|
||||
process_model=process_model,
|
||||
bpmn_xml=bpmn_xml,
|
||||
files=files,
|
||||
current_file_name=current_file_name,
|
||||
)
|
||||
|
||||
|
||||
@admin_blueprint.route(
|
||||
"/process-models/<process_model_id>/<file_name>", methods=["GET"]
|
||||
)
|
||||
def process_model_show_file(process_model_id: str, file_name: str) -> str:
|
||||
"""Process_model_show_file."""
|
||||
process_model = ProcessModelService.get_process_model(process_model_id)
|
||||
bpmn_xml = SpecFileService.get_data(process_model, file_name)
|
||||
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
|
||||
return render_template(
|
||||
"process_model_show.html",
|
||||
process_model=process_model,
|
||||
bpmn_xml=bpmn_xml,
|
||||
files=files,
|
||||
current_file_name=file_name,
|
||||
)
|
||||
|
||||
|
||||
@admin_blueprint.route(
|
||||
"/process-models/<process_model_id>/upload-file", methods=["POST"]
|
||||
)
|
||||
def process_model_upload_file(process_model_id: str) -> Response:
|
||||
"""Process_model_upload_file."""
|
||||
process_model = ProcessModelService.get_process_model(process_model_id)
|
||||
|
||||
if "file" not in request.files:
|
||||
flash("No file part", "error")
|
||||
request_file = request.files["file"]
|
||||
# If the user does not select a file, the browser submits an
|
||||
# empty file without a filename.
|
||||
if request_file.filename == "" or request_file.filename is None:
|
||||
flash("No selected file", "error")
|
||||
else:
|
||||
if request_file and _allowed_file(request_file.filename):
|
||||
if request_file.filename is not None:
|
||||
SpecFileService.add_file(
|
||||
process_model, request_file.filename, request_file.stream.read()
|
||||
)
|
||||
ProcessModelService.save_process_model(process_model)
|
||||
|
||||
return redirect(
|
||||
url_for("admin.process_model_show", process_model_id=process_model.id)
|
||||
)
|
||||
|
||||
|
||||
@admin_blueprint.route(
|
||||
"/process_models/<process_model_id>/edit/<file_name>", methods=["GET"]
|
||||
)
|
||||
def process_model_edit(process_model_id: str, file_name: str) -> str:
|
||||
"""Edit_bpmn."""
|
||||
process_model = ProcessModelService.get_process_model(process_model_id)
|
||||
bpmn_xml = SpecFileService.get_data(process_model, file_name)
|
||||
|
||||
return render_template(
|
||||
"process_model_edit.html",
|
||||
bpmn_xml=bpmn_xml.decode("utf-8"),
|
||||
process_model=process_model,
|
||||
file_name=file_name,
|
||||
)
|
||||
|
||||
|
||||
@admin_blueprint.route(
|
||||
"/process-models/<process_model_id>/save/<file_name>", methods=["POST"]
|
||||
)
|
||||
def process_model_save(process_model_id: str, file_name: str) -> Union[str, Response]:
|
||||
"""Process_model_save."""
|
||||
process_model = ProcessModelService.get_process_model(process_model_id)
|
||||
SpecFileService.update_file(process_model, file_name, request.get_data())
|
||||
if process_model.primary_file_name is None:
|
||||
flash("No primary_file_name", "error")
|
||||
return redirect(url_for("admin.process_group_list"))
|
||||
bpmn_xml = SpecFileService.get_data(process_model, process_model.primary_file_name)
|
||||
return render_template(
|
||||
"process_model_edit.html",
|
||||
bpmn_xml=bpmn_xml.decode("utf-8"),
|
||||
process_model=process_model,
|
||||
file_name=file_name,
|
||||
)
|
||||
|
||||
|
||||
@admin_blueprint.route("/process-models/<process_model_id>/run", methods=["GET"])
|
||||
def process_model_run(process_model_id: str) -> Union[str, Response]:
|
||||
"""Process_model_run."""
|
||||
user = UserService.create_user("Mr. Test", "internal", "Mr. Test")
|
||||
process_instance = (
|
||||
ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
||||
process_model_id, user
|
||||
)
|
||||
)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps()
|
||||
result = processor.get_data()
|
||||
|
||||
process_model = ProcessModelService.get_process_model(process_model_id)
|
||||
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
|
||||
current_file_name = process_model.primary_file_name
|
||||
if current_file_name is None:
|
||||
flash("No primary_file_name", "error")
|
||||
return redirect(url_for("admin.process_group_list"))
|
||||
bpmn_xml = SpecFileService.get_data(process_model, current_file_name)
|
||||
|
||||
return render_template(
|
||||
"process_model_show.html",
|
||||
process_model=process_model,
|
||||
bpmn_xml=bpmn_xml,
|
||||
result=result,
|
||||
files=files,
|
||||
current_file_name=current_file_name,
|
||||
)
|
||||
|
||||
|
||||
# def _find_or_create_user(username: str = "test_user1") -> Any:
|
||||
# """Find_or_create_user."""
|
||||
# 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 _allowed_file(filename: str) -> bool:
|
||||
"""_allowed_file."""
|
||||
return (
|
||||
"." in filename
|
||||
and filename.rsplit(".", 1)[1].lower() in ALLOWED_BPMN_EXTENSIONS
|
||||
)
|
|
@ -1,26 +0,0 @@
|
|||
import BpmnViewer from "bpmn-js";
|
||||
|
||||
var viewer = new BpmnViewer({
|
||||
container: "#canvas",
|
||||
});
|
||||
|
||||
viewer
|
||||
.importXML(pizzaDiagram)
|
||||
.then(function (result) {
|
||||
const { warnings } = result;
|
||||
|
||||
console.log("success !", warnings);
|
||||
|
||||
viewer.get("canvas").zoom("fit-viewport");
|
||||
})
|
||||
.catch(function (err) {
|
||||
const { warnings, message } = err;
|
||||
|
||||
console.log("something went wrong:", warnings, message);
|
||||
});
|
||||
|
||||
export function sayHello() {
|
||||
console.log("hello");
|
||||
}
|
||||
|
||||
window.foo = "bar";
|
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "spiffworkflow-backend",
|
||||
"version": "0.0.0",
|
||||
"description": "Serve up Spiff Workflows to the World!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bpmn-js": "^9.1.0",
|
||||
"bpmn-js-properties-panel": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-cli": "^4.9.2"
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
.example {
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('admin.static', filename='style.css') }}"
|
||||
/>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ self.title() }}</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %} {% if
|
||||
messages %}
|
||||
<ul class="flashes">
|
||||
{% for category, message in messages %}
|
||||
<li class="{{ category }}">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %} {% endwith %} {% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,18 +0,0 @@
|
|||
{% extends "layout.html" %} {% block title %}Process Groups{% endblock %} {%
|
||||
block content %}
|
||||
<table>
|
||||
<tbody>
|
||||
{# here we iterate over every item in our list#} {% for process_group in
|
||||
process_groups %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ url_for('admin.process_group_show', process_group_id=process_group.id) }}"
|
||||
>{{ process_group.display_name }}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -1,25 +0,0 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Process Group: {{ process_group.id }}{% endblock %}
|
||||
{% block content %}
|
||||
<button
|
||||
type="button"
|
||||
onclick="window.location.href='{{ url_for( 'admin.process_group_list') }}';"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<table>
|
||||
<tbody>
|
||||
{# here we iterate over every item in our list#}
|
||||
{% for process_model in process_group.process_models %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ url_for('admin.process_model_show', process_model_id=process_model.id) }}"
|
||||
>{{ process_model.display_name }}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -1,167 +0,0 @@
|
|||
{% extends "layout.html" %} {% block title %}
|
||||
Process Model Edit: {{ process_model.id }}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<!-- example styles -->
|
||||
<!-- required modeler styles -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-js.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/diagram-js.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-font/css/bpmn.css" />
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bpmn-js-properties-panel/dist/assets/properties-panel.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bpmn-js-properties-panel/dist/assets/element-templates.css">
|
||||
|
||||
<!-- modeler distro -->
|
||||
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-modeler.development.js"></script>
|
||||
|
||||
<!-- needed for this example only -->
|
||||
<script src="https://unpkg.com/jquery@3.3.1/dist/jquery.js"></script>
|
||||
|
||||
<!-- example styles -->
|
||||
<style>
|
||||
html, body, #canvas {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.diagram-note {
|
||||
background-color: rgba(66, 180, 21, 0.7);
|
||||
color: White;
|
||||
border-radius: 5px;
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
padding: 5px;
|
||||
min-height: 16px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.needs-discussion:not(.djs-connection) .djs-visual > :nth-child(1) {
|
||||
stroke: rgba(66, 180, 21, 0.7) !important; /* color elements as red */
|
||||
}
|
||||
|
||||
#save-button {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="result">{{ result }}</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick="window.location.href='{{ url_for( 'admin.process_model_show_file', process_model_id=process_model.id, file_name=file_name ) }}';"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button type="button" onclick="exportDiagram()">Save</button>
|
||||
<!-- <div class="modeler"> -->
|
||||
<div id="canvas"></div>
|
||||
<div id="properties"></div>
|
||||
<!-- </div> -->
|
||||
|
||||
<meta id="bpmn_xml" data-name="{{bpmn_xml}}" />
|
||||
<script>
|
||||
// import BpmnModeler from '/admin/static/node_modules/bpmn-js/lib/Modeler.js';
|
||||
// import {
|
||||
// BpmnPropertiesPanelModule,
|
||||
// BpmnPropertiesProviderModule,
|
||||
// } from '/admin/static/node_modules/bpmn-js-properties-panel/dist/index.js';
|
||||
//
|
||||
// const bpmnModeler = new BpmnModeler({
|
||||
// container: '#canvas',
|
||||
// propertiesPanel: {
|
||||
// parent: '#properties'
|
||||
// },
|
||||
// additionalModules: [
|
||||
// BpmnPropertiesPanelModule,
|
||||
// BpmnPropertiesProviderModule
|
||||
// ]
|
||||
// });
|
||||
|
||||
// modeler instance
|
||||
var bpmnModeler = new BpmnJS({
|
||||
container: "#canvas",
|
||||
keyboard: {
|
||||
bindTo: window,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Save diagram contents and print them to the console.
|
||||
*/
|
||||
async function exportDiagram() {
|
||||
try {
|
||||
var data = await bpmnModeler.saveXML({ format: true });
|
||||
//POST request with body equal on data in JSON format
|
||||
fetch("/admin/process-models/{{ process_model.id }}/save/{{ file_name }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/xml",
|
||||
},
|
||||
body: data.xml,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
//Then with the data from the response in JSON...
|
||||
.then((data) => {
|
||||
console.log("Success:", data);
|
||||
})
|
||||
//Then with the error genereted...
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
|
||||
alert("Diagram exported. Check the developer tools!");
|
||||
} catch (err) {
|
||||
console.error("could not save BPMN 2.0 diagram", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open diagram in our modeler instance.
|
||||
*
|
||||
* @param {String} bpmnXML diagram to display
|
||||
*/
|
||||
async function openDiagram(bpmnXML) {
|
||||
// import diagram
|
||||
try {
|
||||
await bpmnModeler.importXML(bpmnXML);
|
||||
|
||||
// access modeler components
|
||||
var canvas = bpmnModeler.get("canvas");
|
||||
var overlays = bpmnModeler.get("overlays");
|
||||
|
||||
// zoom to fit full viewport
|
||||
canvas.zoom("fit-viewport");
|
||||
|
||||
// attach an overlay to a node
|
||||
overlays.add("SCAN_OK", "note", {
|
||||
position: {
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
html: '<div class="diagram-note">Mixed up the labels?</div>',
|
||||
});
|
||||
|
||||
// add marker
|
||||
canvas.addMarker("SCAN_OK", "needs-discussion");
|
||||
} catch (err) {
|
||||
console.error("could not import BPMN 2.0 diagram", err);
|
||||
}
|
||||
}
|
||||
|
||||
// trying to use the python variable bpmn_xml directly causes the xml to have escape sequences
|
||||
// and using the meta tag seems to help with that
|
||||
var bpmn_xml = $("#bpmn_xml").data();
|
||||
openDiagram(bpmn_xml.name);
|
||||
|
||||
// wire save button
|
||||
$("#save-button").click(exportDiagram);
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,159 +0,0 @@
|
|||
{% extends "layout.html" %} {% block title %}Process Model: {{ process_model.id
|
||||
}}{% endblock %} {% block head %} {{ super() }}
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<script src="{{ url_for('admin.static', filename='node_modules/bpmn-js/dist/bpmn-viewer.development.js') }}"></script>
|
||||
|
||||
<!-- viewer distro (without pan and zoom) -->
|
||||
<!--
|
||||
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-viewer.development.js"></script>
|
||||
-->
|
||||
|
||||
<!-- required viewer styles -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-js.css"
|
||||
/>
|
||||
|
||||
<!-- viewer distro (with pan and zoom) -->
|
||||
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-navigated-viewer.development.js"></script>
|
||||
|
||||
<!-- needed for this example only -->
|
||||
<script src="https://unpkg.com/jquery@3.3.1/dist/jquery.js"></script>
|
||||
|
||||
<!-- example styles -->
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#canvas {
|
||||
height: 90%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.diagram-note {
|
||||
background-color: rgba(66, 180, 21, 0.7);
|
||||
color: White;
|
||||
border-radius: 5px;
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
padding: 5px;
|
||||
min-height: 16px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.needs-discussion:not(.djs-connection) .djs-visual > :nth-child(1) {
|
||||
stroke: rgba(66, 180, 21, 0.7) !important; /* color elements as red */
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block content %}
|
||||
<div id="result">{{ result }}</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick="window.location.href='{{ url_for( 'admin.process_group_show', process_group_id=process_model.process_group_id ) }}';"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="window.location.href='{{ url_for( 'admin.process_model_run' , process_model_id=process_model.id ) }}';"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="window.location.href='{{ url_for( 'admin.process_model_edit' , process_model_id=process_model.id, file_name=current_file_name ) }}';"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
{% if files %}
|
||||
<h3>BPMN Files</h3>
|
||||
<ul>
|
||||
{% for file in files %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ url_for('admin.process_model_show_file', process_model_id=process_model.id, file_name=file.name) }}"
|
||||
>{{ file.name }}</a
|
||||
>
|
||||
{% if file.name == current_file_name %} (current) {% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action="/admin/process-models/{{process_model.id}}/upload-file"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
|
||||
<div id="canvas"></div>
|
||||
|
||||
<meta id="bpmn_xml" data-name="{{bpmn_xml}}" />
|
||||
<script>
|
||||
var diagramUrl =
|
||||
"https://cdn.staticaly.com/gh/bpmn-io/bpmn-js-examples/dfceecba/starter/diagram.bpmn";
|
||||
|
||||
// viewer instance
|
||||
var bpmnViewer = new BpmnJS({
|
||||
container: "#canvas",
|
||||
});
|
||||
|
||||
/**
|
||||
* Open diagram in our viewer instance.
|
||||
*
|
||||
* @param {String} bpmnXML diagram to display
|
||||
*/
|
||||
async function openDiagram(bpmnXML) {
|
||||
// import diagram
|
||||
try {
|
||||
await bpmnViewer.importXML(bpmnXML);
|
||||
|
||||
// access viewer components
|
||||
var canvas = bpmnViewer.get("canvas");
|
||||
var overlays = bpmnViewer.get("overlays");
|
||||
|
||||
// zoom to fit full viewport
|
||||
canvas.zoom("fit-viewport");
|
||||
|
||||
// attach an overlay to a node
|
||||
overlays.add("SCAN_OK", "note", {
|
||||
position: {
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
html: '<div class="diagram-note">Mixed up the labels?</div>',
|
||||
});
|
||||
|
||||
// add marker
|
||||
canvas.addMarker("SCAN_OK", "needs-discussion");
|
||||
} catch (err) {
|
||||
console.error("could not import BPMN 2.0 diagram", err);
|
||||
}
|
||||
}
|
||||
var bpmn_xml = $("#bpmn_xml").data();
|
||||
openDiagram(bpmn_xml.name);
|
||||
|
||||
// load external diagram file via AJAX and open it
|
||||
//$.get(diagramUrl, openDiagram, 'text');
|
||||
</script>
|
||||
<!--
|
||||
Thanks for trying out our BPMN toolkit!
|
||||
If you'd like to learn more about what our library,
|
||||
continue with some more basic examples:
|
||||
* https://github.com/bpmn-io/bpmn-js-examples/overlays
|
||||
* https://github.com/bpmn-io/bpmn-js-examples/interaction
|
||||
* https://github.com/bpmn-io/bpmn-js-examples/colors
|
||||
* https://github.com/bpmn-io/bpmn-js-examples/commenting
|
||||
To get a bit broader overview over how bpmn-js works,
|
||||
follow our walkthrough:
|
||||
* https://bpmn.io/toolkit/bpmn-js/walkthrough/
|
||||
Related starters:
|
||||
* https://raw.githubusercontent.com/bpmn-io/bpmn-js-examples/starter/modeler.html
|
||||
-->
|
||||
{% endblock %}
|
|
@ -72,6 +72,18 @@ def process_instance_create(
|
|||
process_model_identifier = _un_modify_modified_process_model_id(
|
||||
modified_process_model_identifier
|
||||
)
|
||||
|
||||
process_model = _get_process_model(process_model_identifier)
|
||||
if process_model.primary_file_name is None:
|
||||
raise ApiError(
|
||||
error_code="process_model_missing_primary_bpmn_file",
|
||||
message=(
|
||||
f"Process Model '{process_model_identifier}' does not have a primary"
|
||||
" bpmn file. One must be set in order to instantiate this model."
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
process_instance = (
|
||||
ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
||||
process_model_identifier, g.user
|
||||
|
@ -102,6 +114,7 @@ def process_instance_run(
|
|||
)
|
||||
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.lock_process_instance("Web")
|
||||
|
||||
if do_engine_steps:
|
||||
try:
|
||||
|
@ -118,6 +131,8 @@ def process_instance_run(
|
|||
status_code=400,
|
||||
task=task,
|
||||
) from e
|
||||
finally:
|
||||
processor.unlock_process_instance("Web")
|
||||
|
||||
if not current_app.config["RUN_BACKGROUND_SCHEDULER"]:
|
||||
MessageService.process_message_instances()
|
||||
|
@ -658,6 +673,9 @@ def _get_process_instance(
|
|||
spec_reference.process_model_id
|
||||
)
|
||||
name_of_file_with_diagram = spec_reference.file_name
|
||||
process_instance.process_model_with_diagram_identifier = (
|
||||
process_model_with_diagram.id
|
||||
)
|
||||
else:
|
||||
process_model_with_diagram = _get_process_model(process_model_identifier)
|
||||
if process_model_with_diagram.primary_file_name:
|
||||
|
@ -679,7 +697,8 @@ def _get_process_instance(
|
|||
)
|
||||
process_instance.bpmn_xml_file_contents = bpmn_xml_file_contents
|
||||
|
||||
return make_response(jsonify(process_instance), 200)
|
||||
process_instance_as_dict = process_instance.serialized_with_metadata()
|
||||
return make_response(jsonify(process_instance_as_dict), 200)
|
||||
|
||||
|
||||
def _find_process_instance_for_me_or_raise(
|
||||
|
|
|
@ -149,7 +149,30 @@ def process_model_update(
|
|||
}
|
||||
|
||||
process_model = _get_process_model(process_model_identifier)
|
||||
|
||||
# FIXME: the logic to update the the process id would be better if it could go into the
|
||||
# process model save method but this causes circular imports with SpecFileService.
|
||||
# All we really need this for is to get the process id from a bpmn file so maybe that could
|
||||
# all be moved to FileSystemService.
|
||||
update_primary_bpmn_file = False
|
||||
if (
|
||||
"primary_file_name" in body_filtered
|
||||
and "primary_process_id" not in body_filtered
|
||||
):
|
||||
if process_model.primary_file_name != body_filtered["primary_file_name"]:
|
||||
update_primary_bpmn_file = True
|
||||
|
||||
ProcessModelService.update_process_model(process_model, body_filtered)
|
||||
|
||||
# update the file to ensure we get the correct process id if the primary file changed.
|
||||
if update_primary_bpmn_file and process_model.primary_file_name:
|
||||
primary_file_contents = SpecFileService.get_data(
|
||||
process_model, process_model.primary_file_name
|
||||
)
|
||||
SpecFileService.update_file(
|
||||
process_model, process_model.primary_file_name, primary_file_contents
|
||||
)
|
||||
|
||||
_commit_and_push_to_git(
|
||||
f"User: {g.user.username} updated process model {process_model_identifier}"
|
||||
)
|
||||
|
@ -277,6 +300,17 @@ def process_model_file_delete(
|
|||
"""Process_model_file_delete."""
|
||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||
process_model = _get_process_model(process_model_identifier)
|
||||
if process_model.primary_file_name == file_name:
|
||||
raise ApiError(
|
||||
error_code="process_model_file_cannot_be_deleted",
|
||||
message=(
|
||||
f"'{file_name}' is the primary bpmn file for"
|
||||
f" '{process_model_identifier}' and cannot be deleted. Please set"
|
||||
" another file as the primary before attempting to delete this one."
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
SpecFileService.delete_file(process_model, file_name)
|
||||
except FileNotFoundError as exception:
|
||||
|
|
|
@ -377,6 +377,7 @@ def task_submit(
|
|||
)
|
||||
)
|
||||
|
||||
processor.lock_process_instance("Web")
|
||||
ProcessInstanceService.complete_form_task(
|
||||
processor=processor,
|
||||
spiff_task=spiff_task,
|
||||
|
@ -384,6 +385,7 @@ def task_submit(
|
|||
user=g.user,
|
||||
human_task=human_task,
|
||||
)
|
||||
processor.unlock_process_instance("Web")
|
||||
|
||||
# If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
|
||||
# task spec, complete that form as well.
|
||||
|
|
|
@ -77,11 +77,17 @@ PATH_SEGMENTS_FOR_PERMISSION_ALL = [
|
|||
]
|
||||
|
||||
|
||||
class UserToGroupDict(TypedDict):
|
||||
username: str
|
||||
group_identifier: str
|
||||
|
||||
|
||||
class DesiredPermissionDict(TypedDict):
|
||||
"""DesiredPermissionDict."""
|
||||
|
||||
group_identifiers: Set[str]
|
||||
permission_assignments: list[PermissionAssignmentModel]
|
||||
user_to_group_identifiers: list[UserToGroupDict]
|
||||
|
||||
|
||||
class AuthorizationService:
|
||||
|
@ -212,6 +218,7 @@ class AuthorizationService:
|
|||
|
||||
default_group = None
|
||||
unique_user_group_identifiers: Set[str] = set()
|
||||
user_to_group_identifiers: list[UserToGroupDict] = []
|
||||
if "default_group" in permission_configs:
|
||||
default_group_identifier = permission_configs["default_group"]
|
||||
default_group = GroupService.find_or_create_group(default_group_identifier)
|
||||
|
@ -231,6 +238,11 @@ class AuthorizationService:
|
|||
)
|
||||
)
|
||||
continue
|
||||
user_to_group_dict: UserToGroupDict = {
|
||||
"username": user.username,
|
||||
"group_identifier": group_identifier,
|
||||
}
|
||||
user_to_group_identifiers.append(user_to_group_dict)
|
||||
cls.associate_user_with_group(user, group)
|
||||
|
||||
permission_assignments = []
|
||||
|
@ -275,6 +287,7 @@ class AuthorizationService:
|
|||
return {
|
||||
"group_identifiers": unique_user_group_identifiers,
|
||||
"permission_assignments": permission_assignments,
|
||||
"user_to_group_identifiers": user_to_group_identifiers,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -735,13 +748,20 @@ class AuthorizationService:
|
|||
def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None:
|
||||
"""Adds new permission assignments and deletes old ones."""
|
||||
initial_permission_assignments = PermissionAssignmentModel.query.all()
|
||||
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all()
|
||||
result = cls.import_permissions_from_yaml_file()
|
||||
desired_permission_assignments = result["permission_assignments"]
|
||||
desired_group_identifiers = result["group_identifiers"]
|
||||
desired_user_to_group_identifiers = result["user_to_group_identifiers"]
|
||||
|
||||
for group in group_info:
|
||||
group_identifier = group["name"]
|
||||
for username in group["users"]:
|
||||
user_to_group_dict: UserToGroupDict = {
|
||||
"username": username,
|
||||
"group_identifier": group_identifier,
|
||||
}
|
||||
desired_user_to_group_identifiers.append(user_to_group_dict)
|
||||
GroupService.add_user_to_group_or_add_to_waiting(
|
||||
username, group_identifier
|
||||
)
|
||||
|
@ -761,6 +781,14 @@ class AuthorizationService:
|
|||
if ipa not in desired_permission_assignments:
|
||||
db.session.delete(ipa)
|
||||
|
||||
for iutga in initial_user_to_group_assignments:
|
||||
current_user_dict: UserToGroupDict = {
|
||||
"username": iutga.user.username,
|
||||
"group_identifier": iutga.group.identifier,
|
||||
}
|
||||
if current_user_dict not in desired_user_to_group_identifiers:
|
||||
db.session.delete(iutga)
|
||||
|
||||
groups_to_delete = GroupModel.query.filter(
|
||||
GroupModel.identifier.not_in(desired_group_identifiers)
|
||||
).all()
|
||||
|
|
|
@ -69,6 +69,7 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import UserTaskConverte
|
|||
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||
from SpiffWorkflow.task import TaskState
|
||||
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
||||
from sqlalchemy import text
|
||||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.models.db import db
|
||||
|
@ -141,6 +142,14 @@ class MissingProcessInfoError(Exception):
|
|||
"""MissingProcessInfoError."""
|
||||
|
||||
|
||||
class ProcessInstanceIsAlreadyLockedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProcessInstanceLockedBySomethingElseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
||||
"""This is a custom script processor that can be easily injected into Spiff Workflow.
|
||||
|
||||
|
@ -761,6 +770,10 @@ class ProcessInstanceProcessor:
|
|||
complete_states = [TaskState.CANCELLED, TaskState.COMPLETED]
|
||||
user_tasks = list(self.get_all_user_tasks())
|
||||
self.process_instance_model.status = self.get_status().value
|
||||
current_app.logger.debug(
|
||||
f"the_status: {self.process_instance_model.status} for instance"
|
||||
f" {self.process_instance_model.id}"
|
||||
)
|
||||
self.process_instance_model.total_tasks = len(user_tasks)
|
||||
self.process_instance_model.completed_tasks = sum(
|
||||
1 for t in user_tasks if t.state in complete_states
|
||||
|
@ -777,7 +790,7 @@ class ProcessInstanceProcessor:
|
|||
db.session.commit()
|
||||
|
||||
human_tasks = HumanTaskModel.query.filter_by(
|
||||
process_instance_id=self.process_instance_model.id
|
||||
process_instance_id=self.process_instance_model.id, completed=False
|
||||
).all()
|
||||
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
|
||||
process_model_display_name = ""
|
||||
|
@ -1142,7 +1155,55 @@ class ProcessInstanceProcessor:
|
|||
|
||||
def get_status(self) -> ProcessInstanceStatus:
|
||||
"""Get_status."""
|
||||
return self.status_of(self.bpmn_process_instance)
|
||||
the_status = self.status_of(self.bpmn_process_instance)
|
||||
# current_app.logger.debug(f"the_status: {the_status} for instance {self.process_instance_model.id}")
|
||||
return the_status
|
||||
|
||||
# inspiration from https://github.com/collectiveidea/delayed_job_active_record/blob/master/lib/delayed/backend/active_record.rb
|
||||
# could consider borrowing their "cleanup all my locks when the app quits" idea as well and
|
||||
# implement via https://docs.python.org/3/library/atexit.html
|
||||
def lock_process_instance(self, lock_prefix: str) -> None:
|
||||
locked_by = f"{lock_prefix}_{current_app.config['PROCESS_UUID']}"
|
||||
current_time_in_seconds = round(time.time())
|
||||
lock_expiry_in_seconds = (
|
||||
current_time_in_seconds
|
||||
- current_app.config["ALLOW_CONFISCATING_LOCK_AFTER_SECONDS"]
|
||||
)
|
||||
|
||||
query_text = text(
|
||||
"UPDATE process_instance SET locked_at_in_seconds ="
|
||||
" :current_time_in_seconds, locked_by = :locked_by where id = :id AND"
|
||||
" (locked_by IS NULL OR locked_at_in_seconds < :lock_expiry_in_seconds);"
|
||||
).execution_options(autocommit=True)
|
||||
result = db.engine.execute(
|
||||
query_text,
|
||||
id=self.process_instance_model.id,
|
||||
current_time_in_seconds=current_time_in_seconds,
|
||||
locked_by=locked_by,
|
||||
lock_expiry_in_seconds=lock_expiry_in_seconds,
|
||||
)
|
||||
# it seems like autocommit is working above (we see the statement in debug logs) but sqlalchemy doesn't
|
||||
# seem to update properly so tell it to commit as well.
|
||||
# if we omit this line then querying the record from a unit test doesn't ever show the record as locked.
|
||||
db.session.commit()
|
||||
if result.rowcount < 1:
|
||||
raise ProcessInstanceIsAlreadyLockedError(
|
||||
f"Cannot lock process instance {self.process_instance_model.id}."
|
||||
"It has already been locked."
|
||||
)
|
||||
|
||||
def unlock_process_instance(self, lock_prefix: str) -> None:
|
||||
locked_by = f"{lock_prefix}_{current_app.config['PROCESS_UUID']}"
|
||||
if self.process_instance_model.locked_by != locked_by:
|
||||
raise ProcessInstanceLockedBySomethingElseError(
|
||||
f"Cannot unlock process instance {self.process_instance_model.id}."
|
||||
f"It locked by {self.process_instance_model.locked_by}"
|
||||
)
|
||||
|
||||
self.process_instance_model.locked_by = None
|
||||
self.process_instance_model.locked_at_in_seconds = None
|
||||
db.session.add(self.process_instance_model)
|
||||
db.session.commit()
|
||||
|
||||
# messages have one correlation key (possibly wrong)
|
||||
# correlation keys may have many correlation properties
|
||||
|
|
|
@ -20,6 +20,9 @@ from spiffworkflow_backend.models.user import UserModel
|
|||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||
from spiffworkflow_backend.services.git_service import GitService
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceIsAlreadyLockedError,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceProcessor,
|
||||
)
|
||||
|
@ -74,12 +77,18 @@ class ProcessInstanceService:
|
|||
.all()
|
||||
)
|
||||
for process_instance in records:
|
||||
locked = False
|
||||
processor = None
|
||||
try:
|
||||
current_app.logger.info(
|
||||
f"Processing process_instance {process_instance.id}"
|
||||
)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.lock_process_instance("Web")
|
||||
locked = True
|
||||
processor.do_engine_steps(save=True)
|
||||
except ProcessInstanceIsAlreadyLockedError:
|
||||
continue
|
||||
except Exception as e:
|
||||
db.session.rollback() # in case the above left the database with a bad transaction
|
||||
process_instance.status = ProcessInstanceStatus.error.value
|
||||
|
@ -91,6 +100,9 @@ class ProcessInstanceService:
|
|||
+ f"({process_instance.process_model_identifier}). {str(e)}"
|
||||
)
|
||||
current_app.logger.error(error_message)
|
||||
finally:
|
||||
if locked and processor:
|
||||
processor.unlock_process_instance("Web")
|
||||
|
||||
@staticmethod
|
||||
def processor_to_process_instance_api(
|
||||
|
@ -220,6 +232,8 @@ class ProcessInstanceService:
|
|||
spiff_task.update_data(dot_dct)
|
||||
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
|
||||
processor.complete_task(spiff_task, human_task, user=user)
|
||||
|
||||
# maybe move this out once we have the interstitial page since this is here just so we can get the next human task
|
||||
processor.do_engine_steps(save=True)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<?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="Process_jm3qjay" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_1w7l0lj</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1w7l0lj" sourceRef="StartEvent_1" targetRef="manual_task" />
|
||||
<bpmn:exclusiveGateway id="loopback_gateway" default="flow_default">
|
||||
<bpmn:incoming>Flow_1ouak9p</bpmn:incoming>
|
||||
<bpmn:outgoing>flow_default</bpmn:outgoing>
|
||||
<bpmn:outgoing>flow_x_equals_one</bpmn:outgoing>
|
||||
</bpmn:exclusiveGateway>
|
||||
<bpmn:sequenceFlow id="Flow_1ouak9p" sourceRef="manual_task" targetRef="loopback_gateway" />
|
||||
<bpmn:endEvent id="Event_1we3snj">
|
||||
<bpmn:incoming>flow_default</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="flow_default" sourceRef="loopback_gateway" targetRef="Event_1we3snj" />
|
||||
<bpmn:manualTask id="manual_task" name="Manual task">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:instructionsForEndUser>HEY</spiffworkflow:instructionsForEndUser>
|
||||
<spiffworkflow:preScript>x = 1</spiffworkflow:preScript>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1w7l0lj</bpmn:incoming>
|
||||
<bpmn:incoming>flow_x_equals_one</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1ouak9p</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="flow_x_equals_one" sourceRef="loopback_gateway" targetRef="manual_task">
|
||||
<bpmn:conditionExpression>x == 1</bpmn:conditionExpression>
|
||||
</bpmn:sequenceFlow>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_jm3qjay">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_15tztve_di" bpmnElement="loopback_gateway" isMarkerVisible="true">
|
||||
<dc:Bounds x="425" y="152" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1we3snj_di" bpmnElement="Event_1we3snj">
|
||||
<dc:Bounds x="532" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1apgvvn_di" bpmnElement="manual_task">
|
||||
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_1w7l0lj_di" bpmnElement="Flow_1w7l0lj">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="270" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ouak9p_di" bpmnElement="Flow_1ouak9p">
|
||||
<di:waypoint x="370" y="177" />
|
||||
<di:waypoint x="425" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0icwqfm_di" bpmnElement="flow_default">
|
||||
<di:waypoint x="475" y="177" />
|
||||
<di:waypoint x="532" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0jnhclm_di" bpmnElement="flow_x_equals_one">
|
||||
<di:waypoint x="450" y="152" />
|
||||
<di:waypoint x="450" y="100" />
|
||||
<di:waypoint x="348" y="137" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -922,6 +922,28 @@ class TestProcessApi(BaseTest):
|
|||
assert response.json is not None
|
||||
assert response.json["error_code"] == "process_model_file_cannot_be_found"
|
||||
|
||||
def test_process_model_file_delete_when_primary_file(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
process_model_identifier = self.create_group_and_model_with_bpmn(
|
||||
client, with_super_admin_user
|
||||
)
|
||||
modified_process_model_identifier = process_model_identifier.replace("/", ":")
|
||||
|
||||
response = client.delete(
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/files/random_fact.bpmn",
|
||||
follow_redirects=True,
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json is not None
|
||||
assert response.json["error_code"] == "process_model_file_cannot_be_deleted"
|
||||
|
||||
def test_process_model_file_delete(
|
||||
self,
|
||||
app: Flask,
|
||||
|
@ -935,8 +957,16 @@ class TestProcessApi(BaseTest):
|
|||
)
|
||||
modified_process_model_identifier = process_model_identifier.replace("/", ":")
|
||||
|
||||
self.create_spec_file(
|
||||
client,
|
||||
process_model_id=process_model_identifier,
|
||||
file_name="second_file.json",
|
||||
file_data=b"<h1>HEY</h1>",
|
||||
user=with_super_admin_user,
|
||||
)
|
||||
|
||||
response = client.delete(
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/files/random_fact.bpmn",
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/files/second_file.json",
|
||||
follow_redirects=True,
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
|
@ -946,7 +976,7 @@ class TestProcessApi(BaseTest):
|
|||
assert response.json["ok"]
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/files/random_fact.svg",
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/files/second_file.json",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""Test_get_localtime."""
|
||||
from operator import itemgetter
|
||||
|
||||
from flask.app import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
|
@ -57,4 +59,8 @@ class TestGetAllPermissions(BaseTest):
|
|||
]
|
||||
|
||||
permissions = GetAllPermissions().run(script_attributes_context)
|
||||
assert permissions == expected_permissions
|
||||
sorted_permissions = sorted(permissions, key=itemgetter("uri"))
|
||||
sorted_expected_permissions = sorted(
|
||||
expected_permissions, key=itemgetter("uri")
|
||||
)
|
||||
assert sorted_permissions == sorted_expected_permissions
|
||||
|
|
|
@ -381,18 +381,27 @@ class TestAuthorizationService(BaseTest):
|
|||
) -> None:
|
||||
"""Test_can_refresh_permissions."""
|
||||
user = self.find_or_create_user(username="user_one")
|
||||
user_two = self.find_or_create_user(username="user_two")
|
||||
admin_user = self.find_or_create_user(username="testadmin1")
|
||||
|
||||
# this group is not mentioned so it will get deleted
|
||||
GroupService.find_or_create_group("group_two")
|
||||
assert GroupModel.query.filter_by(identifier="group_two").first() is not None
|
||||
|
||||
GroupService.find_or_create_group("group_three")
|
||||
assert GroupModel.query.filter_by(identifier="group_three").first() is not None
|
||||
|
||||
group_info = [
|
||||
{
|
||||
"users": ["user_one"],
|
||||
"users": ["user_one", "user_two"],
|
||||
"name": "group_one",
|
||||
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey"}],
|
||||
}
|
||||
},
|
||||
{
|
||||
"users": ["user_two"],
|
||||
"name": "group_three",
|
||||
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey2"}],
|
||||
},
|
||||
]
|
||||
AuthorizationService.refresh_permissions(group_info)
|
||||
assert GroupModel.query.filter_by(identifier="group_two").first() is None
|
||||
|
@ -402,12 +411,32 @@ class TestAuthorizationService(BaseTest):
|
|||
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo")
|
||||
self.assert_user_has_permission(user, "create", "/v1.0/process-groups/hey:yo")
|
||||
|
||||
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey")
|
||||
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey:yo")
|
||||
self.assert_user_has_permission(
|
||||
user_two, "create", "/v1.0/process-groups/hey:yo"
|
||||
)
|
||||
assert GroupModel.query.filter_by(identifier="group_three").first() is not None
|
||||
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey2")
|
||||
self.assert_user_has_permission(
|
||||
user_two, "read", "/v1.0/process-groups/hey2:yo"
|
||||
)
|
||||
self.assert_user_has_permission(
|
||||
user_two, "create", "/v1.0/process-groups/hey2:yo"
|
||||
)
|
||||
|
||||
# remove access to 'hey' from user_two
|
||||
group_info = [
|
||||
{
|
||||
"users": ["user_one"],
|
||||
"name": "group_one",
|
||||
"permissions": [{"actions": ["read"], "uri": "PG:hey"}],
|
||||
}
|
||||
},
|
||||
{
|
||||
"users": ["user_two"],
|
||||
"name": "group_three",
|
||||
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey2"}],
|
||||
},
|
||||
]
|
||||
AuthorizationService.refresh_permissions(group_info)
|
||||
assert GroupModel.query.filter_by(identifier="group_one").first() is not None
|
||||
|
@ -417,3 +446,15 @@ class TestAuthorizationService(BaseTest):
|
|||
user, "create", "/v1.0/process-groups/hey:yo", expected_result=False
|
||||
)
|
||||
self.assert_user_has_permission(admin_user, "create", "/anything-they-want")
|
||||
|
||||
self.assert_user_has_permission(
|
||||
user_two, "read", "/v1.0/process-groups/hey", expected_result=False
|
||||
)
|
||||
assert GroupModel.query.filter_by(identifier="group_three").first() is not None
|
||||
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey2")
|
||||
self.assert_user_has_permission(
|
||||
user_two, "read", "/v1.0/process-groups/hey2:yo"
|
||||
)
|
||||
self.assert_user_has_permission(
|
||||
user_two, "create", "/v1.0/process-groups/hey2:yo"
|
||||
)
|
||||
|
|
|
@ -7,12 +7,19 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
|||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
from spiffworkflow_backend.models.group import GroupModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.authorization_service import (
|
||||
UserDoesNotHaveAccessToTaskError,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceIsAlreadyLockedError,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceLockedBySomethingElseError,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceProcessor,
|
||||
)
|
||||
|
@ -170,7 +177,6 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||
)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps(save=True)
|
||||
processor.save()
|
||||
|
||||
assert len(process_instance.active_human_tasks) == 1
|
||||
human_task = process_instance.active_human_tasks[0]
|
||||
|
@ -293,3 +299,81 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||
|
||||
assert len(process_instance.active_human_tasks) == 1
|
||||
assert initial_human_task_id == process_instance.active_human_tasks[0].id
|
||||
|
||||
def test_it_can_lock_and_unlock_a_process_instance(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
initiator_user = self.find_or_create_user("initiator_user")
|
||||
process_model = load_test_spec(
|
||||
process_model_id="test_group/model_with_lanes",
|
||||
bpmn_file_name="lanes_with_owner_dict.bpmn",
|
||||
process_model_source_directory="model_with_lanes",
|
||||
)
|
||||
process_instance = self.create_process_instance_from_process_model(
|
||||
process_model=process_model, user=initiator_user
|
||||
)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
assert process_instance.locked_by is None
|
||||
assert process_instance.locked_at_in_seconds is None
|
||||
processor.lock_process_instance("TEST")
|
||||
|
||||
process_instance = ProcessInstanceModel.query.filter_by(
|
||||
id=process_instance.id
|
||||
).first()
|
||||
assert process_instance.locked_by is not None
|
||||
assert process_instance.locked_at_in_seconds is not None
|
||||
|
||||
with pytest.raises(ProcessInstanceIsAlreadyLockedError):
|
||||
processor.lock_process_instance("TEST")
|
||||
|
||||
with pytest.raises(ProcessInstanceLockedBySomethingElseError):
|
||||
processor.unlock_process_instance("TEST2")
|
||||
|
||||
processor.unlock_process_instance("TEST")
|
||||
|
||||
process_instance = ProcessInstanceModel.query.filter_by(
|
||||
id=process_instance.id
|
||||
).first()
|
||||
assert process_instance.locked_by is None
|
||||
assert process_instance.locked_at_in_seconds is None
|
||||
|
||||
def test_it_can_loopback_to_previous_bpmn_task_with_gateway(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
initiator_user = self.find_or_create_user("initiator_user")
|
||||
process_model = load_test_spec(
|
||||
process_model_id="test_group/loopback_to_manual_task",
|
||||
bpmn_file_name="loopback.bpmn",
|
||||
process_model_source_directory="loopback_to_manual_task",
|
||||
)
|
||||
process_instance = self.create_process_instance_from_process_model(
|
||||
process_model=process_model, user=initiator_user
|
||||
)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps(save=True)
|
||||
|
||||
assert len(process_instance.active_human_tasks) == 1
|
||||
assert len(process_instance.human_tasks) == 1
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
|
||||
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
|
||||
human_task_one.task_name, processor.bpmn_process_instance
|
||||
)
|
||||
ProcessInstanceService.complete_form_task(
|
||||
processor, spiff_task, {}, initiator_user, human_task_one
|
||||
)
|
||||
|
||||
assert len(process_instance.active_human_tasks) == 1
|
||||
assert len(process_instance.human_tasks) == 2
|
||||
human_task_two = process_instance.active_human_tasks[0]
|
||||
|
||||
# this is just asserting the way the functionality currently works in spiff.
|
||||
# we would actually expect this to change one day if we stop reusing the same guid
|
||||
# when we re-do a task.
|
||||
assert human_task_two.task_id == human_task_one.task_id
|
||||
|
|
|
@ -115,9 +115,11 @@ export default function ProcessInstanceRun({
|
|||
};
|
||||
|
||||
const processInstanceCreateAndRun = () => {
|
||||
setErrorObject(null);
|
||||
HttpService.makeCallToBackend({
|
||||
path: processInstanceCreatePath,
|
||||
successCallback: processModelRun,
|
||||
failureCallback: setErrorObject,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -173,6 +173,10 @@ h1.with-icons {
|
|||
margin-top: 1.3em;
|
||||
}
|
||||
|
||||
.with-top-margin-for-label-next-to-text-input {
|
||||
margin-top: 2.3em;
|
||||
}
|
||||
|
||||
.with-tiny-top-margin {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
|
|
@ -66,6 +66,12 @@ export interface ProcessFile {
|
|||
file_contents?: string;
|
||||
}
|
||||
|
||||
export interface ProcessInstanceMetadata {
|
||||
id: number;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ProcessInstance {
|
||||
id: number;
|
||||
process_model_identifier: string;
|
||||
|
@ -80,6 +86,8 @@ export interface ProcessInstance {
|
|||
updated_at_in_seconds: number;
|
||||
bpmn_version_control_identifier: string;
|
||||
bpmn_version_control_type: string;
|
||||
process_metadata?: ProcessInstanceMetadata[];
|
||||
process_model_with_diagram_identifier?: string;
|
||||
}
|
||||
|
||||
export interface MessageCorrelationProperties {
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@carbon/react';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { FormField, JsonSchemaForm } from '../interfaces';
|
||||
import Form from '../themes/carbon';
|
||||
import { Form } from '../themes/carbon';
|
||||
import {
|
||||
modifyProcessIdentifierForPathParam,
|
||||
slugifyString,
|
||||
|
|
|
@ -35,6 +35,7 @@ import HttpService from '../services/HttpService';
|
|||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
import {
|
||||
convertSecondsToFormattedDateTime,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
unModifyProcessIdentifierForPathParam,
|
||||
} from '../helpers';
|
||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
|
@ -43,6 +44,7 @@ import {
|
|||
PermissionsToCheck,
|
||||
ProcessData,
|
||||
ProcessInstance,
|
||||
ProcessInstanceMetadata,
|
||||
ProcessInstanceTask,
|
||||
} from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
|
@ -74,6 +76,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
|
||||
useState<boolean>(false);
|
||||
const [displayDetails, setDisplayDetails] = useState<boolean>(false);
|
||||
const [showProcessInstanceMetadata, setShowProcessInstanceMetadata] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const { addError, removeError } = useAPIError();
|
||||
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
|
||||
|
@ -391,6 +395,23 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
{processInstance.process_initiator_username}
|
||||
</Column>
|
||||
</Grid>
|
||||
{processInstance.process_model_with_diagram_identifier ? (
|
||||
<Grid condensed fullWidth>
|
||||
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
||||
Current Diagram:{' '}
|
||||
</Column>
|
||||
<Column sm={4} md={6} lg={8} className="grid-date">
|
||||
<Link
|
||||
data-qa="go-to-current-diagram-process-model"
|
||||
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
|
||||
processInstance.process_model_with_diagram_identifier || ''
|
||||
)}`}
|
||||
>
|
||||
{processInstance.process_model_with_diagram_identifier}
|
||||
</Link>
|
||||
</Column>
|
||||
</Grid>
|
||||
) : null}
|
||||
<Grid condensed fullWidth>
|
||||
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
||||
Started:{' '}
|
||||
|
@ -445,6 +466,19 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
Messages
|
||||
</Button>
|
||||
</Can>
|
||||
{processInstance.process_metadata &&
|
||||
processInstance.process_metadata.length > 0 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="button-white-background"
|
||||
data-qa="process-instance-show-metadata"
|
||||
onClick={() => {
|
||||
setShowProcessInstanceMetadata(true);
|
||||
}}
|
||||
>
|
||||
Metadata
|
||||
</Button>
|
||||
) : null}
|
||||
</ButtonSet>
|
||||
</Column>
|
||||
</Grid>
|
||||
|
@ -899,6 +933,41 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
);
|
||||
};
|
||||
|
||||
const processInstanceMetadataArea = () => {
|
||||
if (
|
||||
!processInstance ||
|
||||
(processInstance.process_metadata &&
|
||||
processInstance.process_metadata.length < 1)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const metadataComponents: any[] = [];
|
||||
(processInstance.process_metadata || []).forEach(
|
||||
(processInstanceMetadata: ProcessInstanceMetadata) => {
|
||||
metadataComponents.push(
|
||||
<Grid condensed fullWidth>
|
||||
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
||||
{processInstanceMetadata.key}
|
||||
</Column>
|
||||
<Column sm={3} md={3} lg={3} className="grid-date">
|
||||
{processInstanceMetadata.value}
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
open={showProcessInstanceMetadata}
|
||||
modalHeading="Metadata"
|
||||
passiveModal
|
||||
onRequestClose={() => setShowProcessInstanceMetadata(false)}
|
||||
>
|
||||
{metadataComponents}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const taskUpdateDisplayArea = () => {
|
||||
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
|
||||
const candidateEvents: any = getEvents(taskToUse);
|
||||
|
@ -1030,6 +1099,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
<br />
|
||||
{taskUpdateDisplayArea()}
|
||||
{processDataDisplayArea()}
|
||||
{processInstanceMetadataArea()}
|
||||
{stepsElement()}
|
||||
<br />
|
||||
<ReactDiagramEditor
|
||||
|
|
|
@ -14,6 +14,9 @@ import {
|
|||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
TextInput,
|
||||
Grid,
|
||||
Column,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
|
@ -60,6 +63,8 @@ export default function ProcessModelEditDiagram() {
|
|||
const [processes, setProcesses] = useState<ProcessReference[]>([]);
|
||||
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
|
||||
useState<boolean>(false);
|
||||
const [processModelFileInvalidText, setProcessModelFileInvalidText] =
|
||||
useState<string>('');
|
||||
|
||||
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
|
||||
|
||||
|
@ -160,6 +165,7 @@ export default function ProcessModelEditDiagram() {
|
|||
const handleFileNameCancel = () => {
|
||||
setShowFileNameEditor(false);
|
||||
setNewFileName('');
|
||||
setProcessModelFileInvalidText('');
|
||||
};
|
||||
|
||||
const navigateToProcessModelFile = (_result: any) => {
|
||||
|
@ -251,6 +257,11 @@ export default function ProcessModelEditDiagram() {
|
|||
|
||||
const handleFileNameSave = (event: any) => {
|
||||
event.preventDefault();
|
||||
if (!newFileName) {
|
||||
setProcessModelFileInvalidText('Process Model file name is required.');
|
||||
return;
|
||||
}
|
||||
setProcessModelFileInvalidText('');
|
||||
setShowFileNameEditor(false);
|
||||
saveDiagram(bpmnXmlForDiagramRendering);
|
||||
};
|
||||
|
@ -267,17 +278,32 @@ export default function ProcessModelEditDiagram() {
|
|||
onRequestSubmit={handleFileNameSave}
|
||||
onRequestClose={handleFileNameCancel}
|
||||
>
|
||||
<label>File Name:</label>
|
||||
<span>
|
||||
<input
|
||||
name="file_name"
|
||||
type="text"
|
||||
<Grid
|
||||
condensed
|
||||
fullWidth
|
||||
className="megacondensed process-model-files-section"
|
||||
>
|
||||
<Column md={4} lg={8} sm={4}>
|
||||
<TextInput
|
||||
id="process_model_file_name"
|
||||
labelText="File Name:"
|
||||
value={newFileName}
|
||||
onChange={(e) => setNewFileName(e.target.value)}
|
||||
onChange={(e: any) => setNewFileName(e.target.value)}
|
||||
invalidText={processModelFileInvalidText}
|
||||
invalid={!!processModelFileInvalidText}
|
||||
size="sm"
|
||||
autoFocus
|
||||
/>
|
||||
</Column>
|
||||
<Column
|
||||
md={4}
|
||||
lg={8}
|
||||
sm={4}
|
||||
className="with-top-margin-for-label-next-to-text-input"
|
||||
>
|
||||
{fileExtension}
|
||||
</span>
|
||||
</Column>
|
||||
</Grid>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue