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
|
rst-directives = deprecated
|
||||||
|
|
||||||
per-file-ignores =
|
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
|
# asserts are ok in tests
|
||||||
spiffworkflow-backend/tests/*:S101
|
spiffworkflow-backend/tests/*:S101,D102,D103,D101
|
||||||
|
|
||||||
# prefer naming functions descriptively rather than forcing comments
|
# 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/bin/keycloak_test_server.py:B950,D
|
||||||
spiffworkflow-backend/conftest.py:S105
|
spiffworkflow-backend/conftest.py:S105
|
||||||
|
@ -34,4 +42,4 @@ per-file-ignores =
|
||||||
# TODO: fix the S issues:
|
# TODO: fix the S issues:
|
||||||
# S607 Starting a process with a partial executable path
|
# 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
|
# 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
|
path: sample-process-models
|
||||||
- name: start_keycloak
|
- name: start_keycloak
|
||||||
working-directory: ./spiffworkflow-backend
|
working-directory: ./spiffworkflow-backend
|
||||||
run: ./keycloak/bin/start_keycloak 5
|
run: ./keycloak/bin/start_keycloak
|
||||||
- name: start_backend
|
- name: start_backend
|
||||||
working-directory: ./spiffworkflow-backend
|
working-directory: ./spiffworkflow-backend
|
||||||
run: ./bin/build_and_run_with_docker_compose
|
run: ./bin/build_and_run_with_docker_compose
|
||||||
|
|
|
@ -1,23 +1,16 @@
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
services:
|
services:
|
||||||
spiffworkflow-db:
|
spiffworkflow-frontend:
|
||||||
container_name: spiffworkflow-db
|
container_name: spiffworkflow-frontend
|
||||||
image: mysql:8.0.29
|
image: ghcr.io/sartography/spiffworkflow-frontend:latest
|
||||||
platform: linux/amd64
|
depends_on:
|
||||||
cap_add:
|
spiffworkflow-backend:
|
||||||
- SYS_NICE
|
condition: service_healthy
|
||||||
restart: "no"
|
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_DATABASE=spiffworkflow_backend_development
|
APPLICATION_ROOT: "/"
|
||||||
- MYSQL_ROOT_PASSWORD=my-secret-pw
|
PORT0: "${SPIFF_FRONTEND_PORT:-8001}"
|
||||||
- MYSQL_TCP_PORT=8003
|
|
||||||
ports:
|
ports:
|
||||||
- "8003"
|
- "${SPIFF_FRONTEND_PORT:-8001}:${SPIFF_FRONTEND_PORT:-8001}/tcp"
|
||||||
healthcheck:
|
|
||||||
test: mysql --user=root --password=my-secret-pw -e 'select 1' spiffworkflow_backend_development
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
spiffworkflow-backend:
|
spiffworkflow-backend:
|
||||||
# container_name: spiffworkflow-backend
|
# container_name: spiffworkflow-backend
|
||||||
|
@ -28,58 +21,69 @@ services:
|
||||||
spiffworkflow-db:
|
spiffworkflow-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
- APPLICATION_ROOT=/
|
APPLICATION_ROOT: "/"
|
||||||
- SPIFFWORKFLOW_BACKEND_ENV=development
|
SPIFFWORKFLOW_BACKEND_ENV: "development"
|
||||||
- FLASK_DEBUG=0
|
FLASK_DEBUG: "0"
|
||||||
- FLASK_SESSION_SECRET_KEY=super_secret_key
|
FLASK_SESSION_SECRET_KEY: "${FLASK_SESSION_SECRET_KEY:-super_secret_key}"
|
||||||
- OPEN_ID_SERVER_URL=http://localhost:8000/openid
|
OPEN_ID_SERVER_URL: "http://localhost:${SPIFF_BACKEND_PORT:-8000}/openid"
|
||||||
- SPIFFWORKFLOW_FRONTEND_URL=http://localhost:8001
|
SPIFFWORKFLOW_FRONTEND_URL: "http://localhost:${SPIFF_FRONTEND_PORT:-8001}"
|
||||||
- SPIFFWORKFLOW_BACKEND_URL=http://localhost:8000
|
# WARNING: Frontend is a static site which assumes frontend port - 1 on localhost.
|
||||||
- SPIFFWORKFLOW_BACKEND_PORT=8000
|
SPIFFWORKFLOW_BACKEND_URL: "http://localhost:${SPIFF_BACKEND_PORT:-8000}"
|
||||||
- SPIFFWORKFLOW_BACKEND_UPGRADE_DB=true
|
SPIFFWORKFLOW_BACKEND_PORT: "${SPIFF_BACKEND_PORT:-8000}"
|
||||||
- SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqlconnector://root:my-secret-pw@spiffworkflow-db:8003/spiffworkflow_backend_development
|
SPIFFWORKFLOW_BACKEND_UPGRADE_DB: "true"
|
||||||
- BPMN_SPEC_ABSOLUTE_DIR=/app/process_models
|
SPIFFWORKFLOW_BACKEND_DATABASE_URI: "mysql+mysqlconnector://root:${SPIFF_MYSQL_PASS:-my-secret-pw}@spiffworkflow-db:${SPIFF_MYSQL_PORT:-8003}/spiffworkflow_backend_development"
|
||||||
- SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=false
|
BPMN_SPEC_ABSOLUTE_DIR: "/app/process_models"
|
||||||
- SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=example.yml
|
SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA: "false"
|
||||||
- RUN_BACKGROUND_SCHEDULER=true
|
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME: "example.yml"
|
||||||
- OPEN_ID_CLIENT_ID=spiffworkflow-backend
|
RUN_BACKGROUND_SCHEDULER: "true"
|
||||||
- OPEN_ID_CLIENT_SECRET_KEY=my_open_id_secret_key
|
OPEN_ID_CLIENT_ID: "spiffworkflow-backend"
|
||||||
|
OPEN_ID_CLIENT_SECRET_KEY: "my_open_id_secret_key"
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "${SPIFF_BACKEND_PORT:-8000}:${SPIFF_BACKEND_PORT:-8000}/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- ./process_models:/app/process_models
|
- ./process_models:/app/process_models
|
||||||
- ./log:/app/log
|
- ./log:/app/log
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: curl localhost:8000/v1.0/status --fail
|
test: "curl localhost:${SPIFF_BACKEND_PORT:-8000}/v1.0/status --fail"
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
spiffworkflow-frontend:
|
|
||||||
container_name: spiffworkflow-frontend
|
|
||||||
image: ghcr.io/sartography/spiffworkflow-frontend
|
|
||||||
environment:
|
|
||||||
- APPLICATION_ROOT=/
|
|
||||||
- PORT0=8001
|
|
||||||
ports:
|
|
||||||
- "8001:8001"
|
|
||||||
|
|
||||||
spiffworkflow-connector:
|
spiffworkflow-connector:
|
||||||
container_name: spiffworkflow-connector
|
container_name: spiffworkflow-connector
|
||||||
image: ghcr.io/sartography/connector-proxy-demo
|
image: ghcr.io/sartography/connector-proxy-demo:latest
|
||||||
environment:
|
environment:
|
||||||
- FLASK_ENV=${FLASK_ENV:-development}
|
FLASK_ENV: "${FLASK_ENV:-development}"
|
||||||
- FLASK_DEBUG=0
|
FLASK_DEBUG: "0"
|
||||||
- FLASK_SESSION_SECRET_KEY=${FLASK_SESSION_SECRET_KEY:-super_secret_key}
|
FLASK_SESSION_SECRET_KEY: "${FLASK_SESSION_SECRET_KEY:-super_secret_key}"
|
||||||
- CONNECTOR_PROXY_PORT=8004
|
CONNECTOR_PROXY_PORT: "${SPIFF_CONNECTOR_PORT:-8004}"
|
||||||
ports:
|
ports:
|
||||||
- "8004:8004"
|
- "${SPIFF_CONNECTOR_PORT:-8004}:${SPIFF_CONNECTOR_PORT:-8004}/tcp"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: curl localhost:8004/liveness --fail
|
test: "curl localhost:${SPIFF_CONNECTOR_PORT:-8004}/liveness --fail"
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 20
|
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:
|
volumes:
|
||||||
spiffworkflow_backend:
|
spiffworkflow_backend:
|
||||||
|
|
|
@ -8,11 +8,19 @@ rst-roles = class,const,func,meth,mod,ref
|
||||||
rst-directives = deprecated
|
rst-directives = deprecated
|
||||||
|
|
||||||
per-file-ignores =
|
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
|
# asserts are ok in tests
|
||||||
tests/*:S101
|
tests/*:S101,D102,D103
|
||||||
|
|
||||||
# prefer naming functions descriptively rather than forcing comments
|
# prefer naming functions descriptively rather than forcing comments
|
||||||
*:D102
|
src/*:D102,D103
|
||||||
|
|
||||||
bin/keycloak_test_server.py:B950,D
|
bin/keycloak_test_server.py:B950,D
|
||||||
conftest.py:S105
|
conftest.py:S105
|
||||||
|
@ -31,4 +39,4 @@ per-file-ignores =
|
||||||
# and ignore long comment line
|
# and ignore long comment line
|
||||||
src/spiffworkflow_backend/services/logging_service.py:N802,B950
|
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
|
"""empty message
|
||||||
|
|
||||||
Revision ID: 22212a7d6505
|
Revision ID: 2ec4222f0012
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2023-01-23 10:59:17.365694
|
Create Date: 2023-01-24 10:31:26.693063
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '22212a7d6505'
|
revision = '2ec4222f0012'
|
||||||
down_revision = None
|
down_revision = None
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = 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_type', sa.String(length=50), nullable=True),
|
||||||
sa.Column('bpmn_version_control_identifier', sa.String(length=255), nullable=True),
|
sa.Column('bpmn_version_control_identifier', sa.String(length=255), nullable=True),
|
||||||
sa.Column('spiff_step', sa.Integer(), 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.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
|
@ -204,8 +206,7 @@ def upgrade():
|
||||||
sa.ForeignKeyConstraint(['completed_by_user_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['completed_by_user_id'], ['user.id'], ),
|
||||||
sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ),
|
sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ),
|
||||||
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
|
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id')
|
||||||
sa.UniqueConstraint('task_id', 'process_instance_id', name='human_task_unique')
|
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_human_task_completed'), 'human_task', ['completed'], unique=False)
|
op.create_index(op.f('ix_human_task_completed'), 'human_task', ['completed'], unique=False)
|
||||||
op.create_table('message_correlation',
|
op.create_table('message_correlation',
|
|
@ -72,7 +72,7 @@ zookeeper = ["kazoo"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "astroid"
|
name = "astroid"
|
||||||
version = "2.13.2"
|
version = "2.12.12"
|
||||||
description = "An abstract syntax tree for Python with inference support."
|
description = "An abstract syntax tree for Python with inference support."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -80,7 +80,7 @@ python-versions = ">=3.7.2"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
lazy-object-proxy = ">=1.4.0"
|
lazy-object-proxy = ">=1.4.0"
|
||||||
typing-extensions = ">=4.0.0"
|
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
|
||||||
wrapt = [
|
wrapt = [
|
||||||
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
|
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
|
||||||
{version = ">=1.14,<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"]
|
fasttext = ["fasttext"]
|
||||||
langdetect = ["langdetect"]
|
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]]
|
[[package]]
|
||||||
name = "distlib"
|
name = "distlib"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -878,20 +867,6 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
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]]
|
[[package]]
|
||||||
name = "itsdangerous"
|
name = "itsdangerous"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
|
@ -1067,7 +1042,7 @@ tests = ["pytest", "pytest-lazy-fixture (>=0.6.2)"]
|
||||||
name = "mccabe"
|
name = "mccabe"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
description = "McCabe checker, plugin for flake8"
|
description = "McCabe checker, plugin for flake8"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
@ -1174,7 +1149,7 @@ flake8 = ">=3.9.1"
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "2.5.2"
|
version = "2.5.2"
|
||||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "pyparsing"
|
name = "pyparsing"
|
||||||
version = "3.0.9"
|
version = "3.0.9"
|
||||||
|
@ -1837,8 +1786,8 @@ lxml = "*"
|
||||||
[package.source]
|
[package.source]
|
||||||
type = "git"
|
type = "git"
|
||||||
url = "https://github.com/sartography/SpiffWorkflow"
|
url = "https://github.com/sartography/SpiffWorkflow"
|
||||||
reference = "main"
|
reference = "450ef3bcd639b6bc1c115fbe35bf3f93946cb0c7"
|
||||||
resolved_reference = "7378639d349ed61d907a6891740760e5eee20d1a"
|
resolved_reference = "450ef3bcd639b6bc1c115fbe35bf3f93946cb0c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "SQLAlchemy"
|
name = "SQLAlchemy"
|
||||||
|
@ -1937,14 +1886,6 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]]
|
[[package]]
|
||||||
name = "tornado"
|
name = "tornado"
|
||||||
version = "6.2"
|
version = "6.2"
|
||||||
|
@ -2217,7 +2158,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = ">=3.9,<3.12"
|
python-versions = ">=3.9,<3.12"
|
||||||
content-hash = "681d047a32e12d71a39ff9de8cd6ceffc41048127243f427b69245d5e071419d"
|
content-hash = "ba797b1ccf2dd8dc50d62ff06f6667f28e241b0a26611192d53abfc75b29a415"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
|
@ -2241,8 +2182,8 @@ apscheduler = [
|
||||||
{file = "APScheduler-3.9.1.post1.tar.gz", hash = "sha256:b2bea0309569da53a7261bfa0ce19c67ddbfe151bda776a6a907579fdbd3eb2a"},
|
{file = "APScheduler-3.9.1.post1.tar.gz", hash = "sha256:b2bea0309569da53a7261bfa0ce19c67ddbfe151bda776a6a907579fdbd3eb2a"},
|
||||||
]
|
]
|
||||||
astroid = [
|
astroid = [
|
||||||
{file = "astroid-2.13.2-py3-none-any.whl", hash = "sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"},
|
{file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"},
|
||||||
{file = "astroid-2.13.2.tar.gz", hash = "sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303"},
|
{file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"},
|
||||||
]
|
]
|
||||||
attrs = [
|
attrs = [
|
||||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
{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-py2.py3-none-any.whl", hash = "sha256:d31659dc806a7d88e2b510b2c74f68b525ae531f145c62a57a99bd616b7f90cf"},
|
||||||
{file = "dateparser-1.1.2.tar.gz", hash = "sha256:3821bf191f95b2658c4abd91571c09821ce7a2bc179bf6cefd8b4515c3ccf9ef"},
|
{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 = [
|
distlib = [
|
||||||
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
||||||
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
|
{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_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_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-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-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-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"},
|
||||||
{file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"},
|
{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_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_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-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-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-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"},
|
||||||
{file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"},
|
{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_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_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-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-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-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"},
|
||||||
{file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"},
|
{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-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
{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 = [
|
itsdangerous = [
|
||||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||||
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
{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_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-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-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-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_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-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"},
|
{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-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
|
||||||
{file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
|
{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 = [
|
pyparsing = [
|
||||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
{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-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
{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 = [
|
tornado = [
|
||||||
{file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"},
|
{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"},
|
{file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"},
|
||||||
|
|
|
@ -28,7 +28,8 @@ flask-migrate = "*"
|
||||||
flask-restful = "*"
|
flask-restful = "*"
|
||||||
werkzeug = "*"
|
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)
|
# 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" }
|
# SpiffWorkflow = {develop = true, path = "../SpiffWorkflow" }
|
||||||
sentry-sdk = "^1.10"
|
sentry-sdk = "^1.10"
|
||||||
sphinx-autoapi = "^2.0"
|
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.helpers.api_version import V1_API_PATH_PREFIX
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
from spiffworkflow_backend.models.db import migrate
|
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 (
|
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import (
|
||||||
openid_blueprint,
|
openid_blueprint,
|
||||||
)
|
)
|
||||||
|
@ -106,7 +105,6 @@ def create_app() -> flask.app.Flask:
|
||||||
|
|
||||||
app.register_blueprint(user_blueprint)
|
app.register_blueprint(user_blueprint)
|
||||||
app.register_blueprint(api_error_blueprint)
|
app.register_blueprint(api_error_blueprint)
|
||||||
app.register_blueprint(admin_blueprint, url_prefix="/admin")
|
|
||||||
app.register_blueprint(openid_blueprint, url_prefix="/openid")
|
app.register_blueprint(openid_blueprint, url_prefix="/openid")
|
||||||
|
|
||||||
# preflight options requests will be allowed if they meet the requirements of the url regex.
|
# preflight options requests will be allowed if they meet the requirements of the url regex.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""__init__.py."""
|
"""__init__.py."""
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
import uuid
|
||||||
|
|
||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
from werkzeug.utils import ImportStringError
|
from werkzeug.utils import ImportStringError
|
||||||
|
@ -96,6 +97,8 @@ def setup_config(app: Flask) -> None:
|
||||||
if app.config["BPMN_SPEC_ABSOLUTE_DIR"] is None:
|
if app.config["BPMN_SPEC_ABSOLUTE_DIR"] is None:
|
||||||
raise ConfigurationError("BPMN_SPEC_ABSOLUTE_DIR config must be set")
|
raise ConfigurationError("BPMN_SPEC_ABSOLUTE_DIR config must be set")
|
||||||
|
|
||||||
|
app.config["PROCESS_UUID"] = uuid.uuid4()
|
||||||
|
|
||||||
setup_database_uri(app)
|
setup_database_uri(app)
|
||||||
setup_logger(app)
|
setup_logger(app)
|
||||||
|
|
||||||
|
|
|
@ -82,3 +82,7 @@ SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID = environ.get(
|
||||||
"SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID",
|
"SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID",
|
||||||
default="Message_SystemMessageNotification",
|
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."""
|
"""HumanTaskModel."""
|
||||||
|
|
||||||
__tablename__ = "human_task"
|
__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)
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
process_instance_id: int = db.Column(
|
process_instance_id: int = db.Column(
|
||||||
|
|
|
@ -75,6 +75,10 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore
|
message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore
|
||||||
message_correlations = relationship("MessageCorrelationModel", 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
|
bpmn_json: str | None = deferred(db.Column(db.JSON)) # type: ignore
|
||||||
start_in_seconds: int | None = db.Column(db.Integer)
|
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)
|
created_at_in_seconds: int = db.Column(db.Integer)
|
||||||
status: str = db.Column(db.String(50))
|
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_type: str = db.Column(db.String(50))
|
||||||
bpmn_version_control_identifier: str = db.Column(db.String(255))
|
bpmn_version_control_identifier: str = db.Column(db.String(255))
|
||||||
spiff_step: int = db.Column(db.Integer)
|
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
|
@property
|
||||||
def serialized(self) -> dict[str, Any]:
|
def serialized(self) -> dict[str, Any]:
|
||||||
"""Return object data in serializeable format."""
|
"""Return object data in serializeable format."""
|
||||||
|
@ -108,6 +117,14 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
||||||
"process_initiator_username": self.process_initiator.username,
|
"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
|
@property
|
||||||
def serialized_flat(self) -> dict:
|
def serialized_flat(self) -> dict:
|
||||||
"""Return object in serializeable format with data merged together with top-level attributes.
|
"""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(
|
process_model_identifier = _un_modify_modified_process_model_id(
|
||||||
modified_process_model_identifier
|
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 = (
|
process_instance = (
|
||||||
ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
||||||
process_model_identifier, g.user
|
process_model_identifier, g.user
|
||||||
|
@ -102,6 +114,7 @@ def process_instance_run(
|
||||||
)
|
)
|
||||||
|
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
|
processor.lock_process_instance("Web")
|
||||||
|
|
||||||
if do_engine_steps:
|
if do_engine_steps:
|
||||||
try:
|
try:
|
||||||
|
@ -118,6 +131,8 @@ def process_instance_run(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
task=task,
|
task=task,
|
||||||
) from e
|
) from e
|
||||||
|
finally:
|
||||||
|
processor.unlock_process_instance("Web")
|
||||||
|
|
||||||
if not current_app.config["RUN_BACKGROUND_SCHEDULER"]:
|
if not current_app.config["RUN_BACKGROUND_SCHEDULER"]:
|
||||||
MessageService.process_message_instances()
|
MessageService.process_message_instances()
|
||||||
|
@ -658,6 +673,9 @@ def _get_process_instance(
|
||||||
spec_reference.process_model_id
|
spec_reference.process_model_id
|
||||||
)
|
)
|
||||||
name_of_file_with_diagram = spec_reference.file_name
|
name_of_file_with_diagram = spec_reference.file_name
|
||||||
|
process_instance.process_model_with_diagram_identifier = (
|
||||||
|
process_model_with_diagram.id
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
process_model_with_diagram = _get_process_model(process_model_identifier)
|
process_model_with_diagram = _get_process_model(process_model_identifier)
|
||||||
if process_model_with_diagram.primary_file_name:
|
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
|
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(
|
def _find_process_instance_for_me_or_raise(
|
||||||
|
|
|
@ -149,7 +149,30 @@ def process_model_update(
|
||||||
}
|
}
|
||||||
|
|
||||||
process_model = _get_process_model(process_model_identifier)
|
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)
|
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(
|
_commit_and_push_to_git(
|
||||||
f"User: {g.user.username} updated process model {process_model_identifier}"
|
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_file_delete."""
|
||||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||||
process_model = _get_process_model(process_model_identifier)
|
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:
|
try:
|
||||||
SpecFileService.delete_file(process_model, file_name)
|
SpecFileService.delete_file(process_model, file_name)
|
||||||
except FileNotFoundError as exception:
|
except FileNotFoundError as exception:
|
||||||
|
|
|
@ -377,6 +377,7 @@ def task_submit(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
processor.lock_process_instance("Web")
|
||||||
ProcessInstanceService.complete_form_task(
|
ProcessInstanceService.complete_form_task(
|
||||||
processor=processor,
|
processor=processor,
|
||||||
spiff_task=spiff_task,
|
spiff_task=spiff_task,
|
||||||
|
@ -384,6 +385,7 @@ def task_submit(
|
||||||
user=g.user,
|
user=g.user,
|
||||||
human_task=human_task,
|
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
|
# 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.
|
# 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):
|
class DesiredPermissionDict(TypedDict):
|
||||||
"""DesiredPermissionDict."""
|
"""DesiredPermissionDict."""
|
||||||
|
|
||||||
group_identifiers: Set[str]
|
group_identifiers: Set[str]
|
||||||
permission_assignments: list[PermissionAssignmentModel]
|
permission_assignments: list[PermissionAssignmentModel]
|
||||||
|
user_to_group_identifiers: list[UserToGroupDict]
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationService:
|
class AuthorizationService:
|
||||||
|
@ -212,6 +218,7 @@ class AuthorizationService:
|
||||||
|
|
||||||
default_group = None
|
default_group = None
|
||||||
unique_user_group_identifiers: Set[str] = set()
|
unique_user_group_identifiers: Set[str] = set()
|
||||||
|
user_to_group_identifiers: list[UserToGroupDict] = []
|
||||||
if "default_group" in permission_configs:
|
if "default_group" in permission_configs:
|
||||||
default_group_identifier = permission_configs["default_group"]
|
default_group_identifier = permission_configs["default_group"]
|
||||||
default_group = GroupService.find_or_create_group(default_group_identifier)
|
default_group = GroupService.find_or_create_group(default_group_identifier)
|
||||||
|
@ -231,6 +238,11 @@ class AuthorizationService:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
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)
|
cls.associate_user_with_group(user, group)
|
||||||
|
|
||||||
permission_assignments = []
|
permission_assignments = []
|
||||||
|
@ -275,6 +287,7 @@ class AuthorizationService:
|
||||||
return {
|
return {
|
||||||
"group_identifiers": unique_user_group_identifiers,
|
"group_identifiers": unique_user_group_identifiers,
|
||||||
"permission_assignments": permission_assignments,
|
"permission_assignments": permission_assignments,
|
||||||
|
"user_to_group_identifiers": user_to_group_identifiers,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -735,13 +748,20 @@ class AuthorizationService:
|
||||||
def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None:
|
def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None:
|
||||||
"""Adds new permission assignments and deletes old ones."""
|
"""Adds new permission assignments and deletes old ones."""
|
||||||
initial_permission_assignments = PermissionAssignmentModel.query.all()
|
initial_permission_assignments = PermissionAssignmentModel.query.all()
|
||||||
|
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all()
|
||||||
result = cls.import_permissions_from_yaml_file()
|
result = cls.import_permissions_from_yaml_file()
|
||||||
desired_permission_assignments = result["permission_assignments"]
|
desired_permission_assignments = result["permission_assignments"]
|
||||||
desired_group_identifiers = result["group_identifiers"]
|
desired_group_identifiers = result["group_identifiers"]
|
||||||
|
desired_user_to_group_identifiers = result["user_to_group_identifiers"]
|
||||||
|
|
||||||
for group in group_info:
|
for group in group_info:
|
||||||
group_identifier = group["name"]
|
group_identifier = group["name"]
|
||||||
for username in group["users"]:
|
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(
|
GroupService.add_user_to_group_or_add_to_waiting(
|
||||||
username, group_identifier
|
username, group_identifier
|
||||||
)
|
)
|
||||||
|
@ -761,6 +781,14 @@ class AuthorizationService:
|
||||||
if ipa not in desired_permission_assignments:
|
if ipa not in desired_permission_assignments:
|
||||||
db.session.delete(ipa)
|
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(
|
groups_to_delete = GroupModel.query.filter(
|
||||||
GroupModel.identifier.not_in(desired_group_identifiers)
|
GroupModel.identifier.not_in(desired_group_identifiers)
|
||||||
).all()
|
).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 Task as SpiffTask # type: ignore
|
||||||
from SpiffWorkflow.task import TaskState
|
from SpiffWorkflow.task import TaskState
|
||||||
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
|
@ -141,6 +142,14 @@ class MissingProcessInfoError(Exception):
|
||||||
"""MissingProcessInfoError."""
|
"""MissingProcessInfoError."""
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessInstanceIsAlreadyLockedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessInstanceLockedBySomethingElseError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
||||||
"""This is a custom script processor that can be easily injected into Spiff Workflow.
|
"""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]
|
complete_states = [TaskState.CANCELLED, TaskState.COMPLETED]
|
||||||
user_tasks = list(self.get_all_user_tasks())
|
user_tasks = list(self.get_all_user_tasks())
|
||||||
self.process_instance_model.status = self.get_status().value
|
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.total_tasks = len(user_tasks)
|
||||||
self.process_instance_model.completed_tasks = sum(
|
self.process_instance_model.completed_tasks = sum(
|
||||||
1 for t in user_tasks if t.state in complete_states
|
1 for t in user_tasks if t.state in complete_states
|
||||||
|
@ -777,7 +790,7 @@ class ProcessInstanceProcessor:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
human_tasks = HumanTaskModel.query.filter_by(
|
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()
|
).all()
|
||||||
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
|
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
|
||||||
process_model_display_name = ""
|
process_model_display_name = ""
|
||||||
|
@ -1142,7 +1155,55 @@ class ProcessInstanceProcessor:
|
||||||
|
|
||||||
def get_status(self) -> ProcessInstanceStatus:
|
def get_status(self) -> ProcessInstanceStatus:
|
||||||
"""Get_status."""
|
"""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)
|
# messages have one correlation key (possibly wrong)
|
||||||
# correlation keys may have many correlation properties
|
# 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.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||||
from spiffworkflow_backend.services.git_service import GitService
|
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 (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
|
@ -74,12 +77,18 @@ class ProcessInstanceService:
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
for process_instance in records:
|
for process_instance in records:
|
||||||
|
locked = False
|
||||||
|
processor = None
|
||||||
try:
|
try:
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"Processing process_instance {process_instance.id}"
|
f"Processing process_instance {process_instance.id}"
|
||||||
)
|
)
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
|
processor.lock_process_instance("Web")
|
||||||
|
locked = True
|
||||||
processor.do_engine_steps(save=True)
|
processor.do_engine_steps(save=True)
|
||||||
|
except ProcessInstanceIsAlreadyLockedError:
|
||||||
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback() # in case the above left the database with a bad transaction
|
db.session.rollback() # in case the above left the database with a bad transaction
|
||||||
process_instance.status = ProcessInstanceStatus.error.value
|
process_instance.status = ProcessInstanceStatus.error.value
|
||||||
|
@ -91,6 +100,9 @@ class ProcessInstanceService:
|
||||||
+ f"({process_instance.process_model_identifier}). {str(e)}"
|
+ f"({process_instance.process_model_identifier}). {str(e)}"
|
||||||
)
|
)
|
||||||
current_app.logger.error(error_message)
|
current_app.logger.error(error_message)
|
||||||
|
finally:
|
||||||
|
if locked and processor:
|
||||||
|
processor.unlock_process_instance("Web")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def processor_to_process_instance_api(
|
def processor_to_process_instance_api(
|
||||||
|
@ -220,6 +232,8 @@ class ProcessInstanceService:
|
||||||
spiff_task.update_data(dot_dct)
|
spiff_task.update_data(dot_dct)
|
||||||
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
|
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
|
||||||
processor.complete_task(spiff_task, human_task, user=user)
|
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)
|
processor.do_engine_steps(save=True)
|
||||||
|
|
||||||
@staticmethod
|
@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 is not None
|
||||||
assert response.json["error_code"] == "process_model_file_cannot_be_found"
|
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(
|
def test_process_model_file_delete(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
|
@ -935,8 +957,16 @@ class TestProcessApi(BaseTest):
|
||||||
)
|
)
|
||||||
modified_process_model_identifier = process_model_identifier.replace("/", ":")
|
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(
|
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,
|
follow_redirects=True,
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
|
@ -946,7 +976,7 @@ class TestProcessApi(BaseTest):
|
||||||
assert response.json["ok"]
|
assert response.json["ok"]
|
||||||
|
|
||||||
response = client.get(
|
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),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""Test_get_localtime."""
|
"""Test_get_localtime."""
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
|
@ -57,4 +59,8 @@ class TestGetAllPermissions(BaseTest):
|
||||||
]
|
]
|
||||||
|
|
||||||
permissions = GetAllPermissions().run(script_attributes_context)
|
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:
|
) -> None:
|
||||||
"""Test_can_refresh_permissions."""
|
"""Test_can_refresh_permissions."""
|
||||||
user = self.find_or_create_user(username="user_one")
|
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")
|
admin_user = self.find_or_create_user(username="testadmin1")
|
||||||
|
|
||||||
# this group is not mentioned so it will get deleted
|
# this group is not mentioned so it will get deleted
|
||||||
GroupService.find_or_create_group("group_two")
|
GroupService.find_or_create_group("group_two")
|
||||||
assert GroupModel.query.filter_by(identifier="group_two").first() is not None
|
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 = [
|
group_info = [
|
||||||
{
|
{
|
||||||
"users": ["user_one"],
|
"users": ["user_one", "user_two"],
|
||||||
"name": "group_one",
|
"name": "group_one",
|
||||||
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey"}],
|
"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)
|
AuthorizationService.refresh_permissions(group_info)
|
||||||
assert GroupModel.query.filter_by(identifier="group_two").first() is None
|
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, "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, "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 = [
|
group_info = [
|
||||||
{
|
{
|
||||||
"users": ["user_one"],
|
"users": ["user_one"],
|
||||||
"name": "group_one",
|
"name": "group_one",
|
||||||
"permissions": [{"actions": ["read"], "uri": "PG:hey"}],
|
"permissions": [{"actions": ["read"], "uri": "PG:hey"}],
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"users": ["user_two"],
|
||||||
|
"name": "group_three",
|
||||||
|
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey2"}],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
AuthorizationService.refresh_permissions(group_info)
|
AuthorizationService.refresh_permissions(group_info)
|
||||||
assert GroupModel.query.filter_by(identifier="group_one").first() is not None
|
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
|
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(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 tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||||
|
|
||||||
from spiffworkflow_backend.models.group import GroupModel
|
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.process_instance import ProcessInstanceStatus
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.authorization_service import (
|
from spiffworkflow_backend.services.authorization_service import (
|
||||||
UserDoesNotHaveAccessToTaskError,
|
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 (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
|
@ -170,7 +177,6 @@ class TestProcessInstanceProcessor(BaseTest):
|
||||||
)
|
)
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
processor.do_engine_steps(save=True)
|
processor.do_engine_steps(save=True)
|
||||||
processor.save()
|
|
||||||
|
|
||||||
assert len(process_instance.active_human_tasks) == 1
|
assert len(process_instance.active_human_tasks) == 1
|
||||||
human_task = process_instance.active_human_tasks[0]
|
human_task = process_instance.active_human_tasks[0]
|
||||||
|
@ -293,3 +299,81 @@ class TestProcessInstanceProcessor(BaseTest):
|
||||||
|
|
||||||
assert len(process_instance.active_human_tasks) == 1
|
assert len(process_instance.active_human_tasks) == 1
|
||||||
assert initial_human_task_id == process_instance.active_human_tasks[0].id
|
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 = () => {
|
const processInstanceCreateAndRun = () => {
|
||||||
|
setErrorObject(null);
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: processInstanceCreatePath,
|
path: processInstanceCreatePath,
|
||||||
successCallback: processModelRun,
|
successCallback: processModelRun,
|
||||||
|
failureCallback: setErrorObject,
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -173,6 +173,10 @@ h1.with-icons {
|
||||||
margin-top: 1.3em;
|
margin-top: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.with-top-margin-for-label-next-to-text-input {
|
||||||
|
margin-top: 2.3em;
|
||||||
|
}
|
||||||
|
|
||||||
.with-tiny-top-margin {
|
.with-tiny-top-margin {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,12 @@ export interface ProcessFile {
|
||||||
file_contents?: string;
|
file_contents?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProcessInstanceMetadata {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProcessInstance {
|
export interface ProcessInstance {
|
||||||
id: number;
|
id: number;
|
||||||
process_model_identifier: string;
|
process_model_identifier: string;
|
||||||
|
@ -80,6 +86,8 @@ export interface ProcessInstance {
|
||||||
updated_at_in_seconds: number;
|
updated_at_in_seconds: number;
|
||||||
bpmn_version_control_identifier: string;
|
bpmn_version_control_identifier: string;
|
||||||
bpmn_version_control_type: string;
|
bpmn_version_control_type: string;
|
||||||
|
process_metadata?: ProcessInstanceMetadata[];
|
||||||
|
process_model_with_diagram_identifier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageCorrelationProperties {
|
export interface MessageCorrelationProperties {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from '@carbon/react';
|
} from '@carbon/react';
|
||||||
import validator from '@rjsf/validator-ajv8';
|
import validator from '@rjsf/validator-ajv8';
|
||||||
import { FormField, JsonSchemaForm } from '../interfaces';
|
import { FormField, JsonSchemaForm } from '../interfaces';
|
||||||
import Form from '../themes/carbon';
|
import { Form } from '../themes/carbon';
|
||||||
import {
|
import {
|
||||||
modifyProcessIdentifierForPathParam,
|
modifyProcessIdentifierForPathParam,
|
||||||
slugifyString,
|
slugifyString,
|
||||||
|
|
|
@ -35,6 +35,7 @@ import HttpService from '../services/HttpService';
|
||||||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||||
import {
|
import {
|
||||||
convertSecondsToFormattedDateTime,
|
convertSecondsToFormattedDateTime,
|
||||||
|
modifyProcessIdentifierForPathParam,
|
||||||
unModifyProcessIdentifierForPathParam,
|
unModifyProcessIdentifierForPathParam,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||||
|
@ -43,6 +44,7 @@ import {
|
||||||
PermissionsToCheck,
|
PermissionsToCheck,
|
||||||
ProcessData,
|
ProcessData,
|
||||||
ProcessInstance,
|
ProcessInstance,
|
||||||
|
ProcessInstanceMetadata,
|
||||||
ProcessInstanceTask,
|
ProcessInstanceTask,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||||
|
@ -74,6 +76,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
|
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [displayDetails, setDisplayDetails] = useState<boolean>(false);
|
const [displayDetails, setDisplayDetails] = useState<boolean>(false);
|
||||||
|
const [showProcessInstanceMetadata, setShowProcessInstanceMetadata] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const { addError, removeError } = useAPIError();
|
const { addError, removeError } = useAPIError();
|
||||||
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
|
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
|
||||||
|
@ -391,6 +395,23 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
{processInstance.process_initiator_username}
|
{processInstance.process_initiator_username}
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</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>
|
<Grid condensed fullWidth>
|
||||||
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
||||||
Started:{' '}
|
Started:{' '}
|
||||||
|
@ -445,6 +466,19 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
Messages
|
Messages
|
||||||
</Button>
|
</Button>
|
||||||
</Can>
|
</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>
|
</ButtonSet>
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</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 taskUpdateDisplayArea = () => {
|
||||||
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
|
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
|
||||||
const candidateEvents: any = getEvents(taskToUse);
|
const candidateEvents: any = getEvents(taskToUse);
|
||||||
|
@ -1030,6 +1099,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
<br />
|
<br />
|
||||||
{taskUpdateDisplayArea()}
|
{taskUpdateDisplayArea()}
|
||||||
{processDataDisplayArea()}
|
{processDataDisplayArea()}
|
||||||
|
{processInstanceMetadataArea()}
|
||||||
{stepsElement()}
|
{stepsElement()}
|
||||||
<br />
|
<br />
|
||||||
<ReactDiagramEditor
|
<ReactDiagramEditor
|
||||||
|
|
|
@ -14,6 +14,9 @@ import {
|
||||||
Tab,
|
Tab,
|
||||||
TabPanels,
|
TabPanels,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
|
TextInput,
|
||||||
|
Grid,
|
||||||
|
Column,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from '@carbon/react';
|
} from '@carbon/react';
|
||||||
import Row from 'react-bootstrap/Row';
|
import Row from 'react-bootstrap/Row';
|
||||||
|
@ -60,6 +63,8 @@ export default function ProcessModelEditDiagram() {
|
||||||
const [processes, setProcesses] = useState<ProcessReference[]>([]);
|
const [processes, setProcesses] = useState<ProcessReference[]>([]);
|
||||||
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
|
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
const [processModelFileInvalidText, setProcessModelFileInvalidText] =
|
||||||
|
useState<string>('');
|
||||||
|
|
||||||
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
|
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
|
||||||
|
|
||||||
|
@ -160,6 +165,7 @@ export default function ProcessModelEditDiagram() {
|
||||||
const handleFileNameCancel = () => {
|
const handleFileNameCancel = () => {
|
||||||
setShowFileNameEditor(false);
|
setShowFileNameEditor(false);
|
||||||
setNewFileName('');
|
setNewFileName('');
|
||||||
|
setProcessModelFileInvalidText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToProcessModelFile = (_result: any) => {
|
const navigateToProcessModelFile = (_result: any) => {
|
||||||
|
@ -251,6 +257,11 @@ export default function ProcessModelEditDiagram() {
|
||||||
|
|
||||||
const handleFileNameSave = (event: any) => {
|
const handleFileNameSave = (event: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!newFileName) {
|
||||||
|
setProcessModelFileInvalidText('Process Model file name is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProcessModelFileInvalidText('');
|
||||||
setShowFileNameEditor(false);
|
setShowFileNameEditor(false);
|
||||||
saveDiagram(bpmnXmlForDiagramRendering);
|
saveDiagram(bpmnXmlForDiagramRendering);
|
||||||
};
|
};
|
||||||
|
@ -267,17 +278,32 @@ export default function ProcessModelEditDiagram() {
|
||||||
onRequestSubmit={handleFileNameSave}
|
onRequestSubmit={handleFileNameSave}
|
||||||
onRequestClose={handleFileNameCancel}
|
onRequestClose={handleFileNameCancel}
|
||||||
>
|
>
|
||||||
<label>File Name:</label>
|
<Grid
|
||||||
<span>
|
condensed
|
||||||
<input
|
fullWidth
|
||||||
name="file_name"
|
className="megacondensed process-model-files-section"
|
||||||
type="text"
|
>
|
||||||
value={newFileName}
|
<Column md={4} lg={8} sm={4}>
|
||||||
onChange={(e) => setNewFileName(e.target.value)}
|
<TextInput
|
||||||
autoFocus
|
id="process_model_file_name"
|
||||||
/>
|
labelText="File Name:"
|
||||||
{fileExtension}
|
value={newFileName}
|
||||||
</span>
|
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}
|
||||||
|
</Column>
|
||||||
|
</Grid>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue