From 0db1727a991524d584acfe18348fd33d8b4d41a7 Mon Sep 17 00:00:00 2001 From: jasquat <2487833+jasquat@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:00:03 +0000 Subject: [PATCH] Bugfix/data object management (#1243) * initial updates to prepare for data object data migration w/ burnettk * added method to import bpmn_process_dict to the database w/ burnettk * test to ensure we can import json is passing w/ burnettk * added some more tests around importing bpmn process json w/ burnettk * version 4 migration test is now passing w/ burnettk * spiff lib back to main after its merge * some coderabbit suggestions * do not run version 3 and 4 migrations twice w/ burnettk * build docker images for this branch w/ burnettk --------- Co-authored-by: jasquat Co-authored-by: burnettk --- .../docker_image_for_main_builds.yml | 2 +- spiffworkflow-backend/poetry.lock | 31 +- .../src/spiffworkflow_backend/constants.py | 1 + .../process_instance_migrator.py | 9 +- .../data_migrations/version_4.py | 33 + .../models/bpmn_process_definition.py | 4 + .../routes/process_api_blueprint.py | 3 +- .../routes/tasks_controller.py | 3 +- .../services/process_instance_processor.py | 91 +- .../services/task_service.py | 39 +- .../data/service-task-with-data-obj/main.bpmn | 80 ++ .../without-service-task.bpmn | 163 +++ ...ocess_instance_data_objects_version_3.json | 966 ++++++++++++++++++ .../helpers/base_test.py | 17 + .../unit/test_process_instance_migrator.py | 79 ++ .../unit/test_process_instance_processor.py | 67 ++ .../src/routes/BaseRoutes.tsx | 12 +- 17 files changed, 1547 insertions(+), 53 deletions(-) create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/constants.py create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/version_4.py create mode 100644 spiffworkflow-backend/tests/data/service-task-with-data-obj/main.bpmn create mode 100644 spiffworkflow-backend/tests/data/service-task-with-data-obj/without-service-task.bpmn create mode 100644 spiffworkflow-backend/tests/files/bpmn_process_instance_data_objects_version_3.json diff --git a/.github/workflows/docker_image_for_main_builds.yml b/.github/workflows/docker_image_for_main_builds.yml index 79b15eb89..5b8d44c63 100644 --- a/.github/workflows/docker_image_for_main_builds.yml +++ b/.github/workflows/docker_image_for_main_builds.yml @@ -31,7 +31,7 @@ on: branches: - main - spiffdemo - - feature/update-extension-docs + - bugfix/data-object-management jobs: create_frontend_docker_image: diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index d3f6a4f8e..469dfdce2 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -1945,6 +1945,8 @@ files = [ {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, + {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, + {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, @@ -2284,6 +2286,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2660,37 +2663,51 @@ python-versions = ">=3.6" files = [ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d92f81886165cb14d7b067ef37e142256f1c6a90a65cd156b063a43da1708cfd"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b5edda50e5e9e15e54a6a8a0070302b00c518a9d32accc2346ad6c984aacd279"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:7048c338b6c86627afb27faecf418768acb6331fc24cfa56c93e8c9780f815fa"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3fcc54cb0c8b811ff66082de1680b4b14cf8a81dce0d4fbf665c2265a81e07a1"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:665f58bfd29b167039f714c6998178d27ccd83984084c286110ef26b230f259f"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9eb5dee2772b0f704ca2e45b1713e4e5198c18f515b52743576d196348f374d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, @@ -2975,7 +2992,7 @@ doc = ["sphinx", "sphinx_rtd_theme"] type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "32c00e4a276b931aace9c9e9491cdb18011e6780" +resolved_reference = "86cb84d29cb25c1a5a407a702ef35cf7b469df6b" [[package]] name = "spiffworkflow-connector-command" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/constants.py b/spiffworkflow-backend/src/spiffworkflow_backend/constants.py new file mode 100644 index 000000000..ac05988ee --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/constants.py @@ -0,0 +1 @@ +SPIFFWORKFLOW_BACKEND_SERIALIZER_VERSION = "4" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/process_instance_migrator.py b/spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/process_instance_migrator.py index f476b85e8..a2b940310 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/process_instance_migrator.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/process_instance_migrator.py @@ -5,6 +5,7 @@ from flask import current_app from spiffworkflow_backend.data_migrations.data_migration_base import DataMigrationBase from spiffworkflow_backend.data_migrations.version_2 import Version2 from spiffworkflow_backend.data_migrations.version_3 import Version3 +from spiffworkflow_backend.data_migrations.version_4 import Version4 from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.process_instance import ProcessInstanceModel @@ -29,8 +30,6 @@ def benchmark_log_func(func: Any) -> Any: class ProcessInstanceMigrator: - CURRENT_VERSION = "3" - @classmethod def run(cls, process_instance: ProcessInstanceModel) -> None: """This updates the serialization of an instance to the current expected state. @@ -52,8 +51,12 @@ class ProcessInstanceMigrator: if process_instance.spiff_serializer_version < Version2.version(): cls.run_version(Version3, process_instance) cls.run_version(Version2, process_instance) - else: + cls.run_version(Version4, process_instance) + elif process_instance.spiff_serializer_version < Version3.version(): cls.run_version(Version3, process_instance) + cls.run_version(Version4, process_instance) + else: + cls.run_version(Version4, process_instance) @classmethod @benchmark_log_func diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/version_4.py b/spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/version_4.py new file mode 100644 index 000000000..cf449fbc4 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/data_migrations/version_4.py @@ -0,0 +1,33 @@ +from flask import current_app +from SpiffWorkflow.bpmn.serializer.migration.version_1_3 import update_data_objects # type: ignore +from SpiffWorkflow.task import Task as SpiffTask # type: ignore +from spiffworkflow_backend.data_migrations.data_migration_base import DataMigrationBase +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor +from spiffworkflow_backend.services.task_service import TaskService + + +class Version4(DataMigrationBase): + @classmethod + def version(cls) -> str: + return "4" + + @classmethod + def run(cls, process_instance: ProcessInstanceModel) -> None: + # return None + try: + processor = ProcessInstanceProcessor(process_instance) + bpmn_process_dict = processor.serialize() + update_data_objects(bpmn_process_dict) + ProcessInstanceProcessor.persist_bpmn_process_dict( + bpmn_process_dict, bpmn_definition_to_task_definitions_mappings={}, process_instance_model=process_instance + ) + + except Exception as ex: + current_app.logger.warning(f"Failed to migrate process_instance '{process_instance.id}'. The error was {str(ex)}") + + @classmethod + def update_spiff_task_parents(cls, spiff_task: SpiffTask, task_service: TaskService) -> None: + task_service.update_task_model_with_spiff_task(spiff_task) + if spiff_task.parent is not None: + cls.update_spiff_task_parents(spiff_task.parent, task_service) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition.py index 77aa6703d..0bbaf180a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition.py @@ -48,3 +48,7 @@ class BpmnProcessDefinitionModel(SpiffworkflowBaseDBModel): updated_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer) + + @classmethod + def keys_for_full_process_model_hash(cls) -> list[str]: + return ["spec", "subprocess_specs", "serializer_version"] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index aee1c7ee9..a9cf7dfbf 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -167,6 +167,7 @@ def _process_data_fetcher( data_objects = bpmn_process_instance.spec.data_objects data_object = data_objects.get(process_data_identifier) + if data_object is None: raise ApiError( error_code="data_object_not_found", @@ -185,7 +186,7 @@ def _process_data_fetcher( status_code=400, ) - process_data_value = bpmn_process_data.get(process_data_identifier) + process_data_value = bpmn_process_data.get("data_objects", bpmn_process_data).get(process_data_identifier) return make_response( jsonify( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 1427d5003..e60abb8d8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -22,6 +22,7 @@ from sqlalchemy import func from sqlalchemy.orm import aliased from sqlalchemy.orm.util import AliasedClass +from spiffworkflow_backend.constants import SPIFFWORKFLOW_BACKEND_SERIALIZER_VERSION from spiffworkflow_backend.data_migrations.process_instance_migrator import ProcessInstanceMigrator from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedError @@ -624,7 +625,7 @@ def _dequeued_interstitial_stream( # attempt to run the migrator even for a readonly operation if the process instance is not newest if ( process_instance.spiff_serializer_version is not None - and process_instance.spiff_serializer_version < ProcessInstanceMigrator.CURRENT_VERSION + and process_instance.spiff_serializer_version < SPIFFWORKFLOW_BACKEND_SERIALIZER_VERSION ): try: with ProcessInstanceQueueService.dequeued(process_instance): diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index e8d3c376b..51062bb3c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -53,6 +53,7 @@ from SpiffWorkflow.util.task import TaskIterator # type: ignore from SpiffWorkflow.util.task import TaskState from sqlalchemy import and_ +from spiffworkflow_backend.constants import SPIFFWORKFLOW_BACKEND_SERIALIZER_VERSION from spiffworkflow_backend.data_stores.json import JSONDataStore from spiffworkflow_backend.data_stores.json import JSONDataStoreConverter from spiffworkflow_backend.data_stores.json import JSONFileDataStore @@ -402,10 +403,9 @@ IdToBpmnProcessSpecMapping = NewType("IdToBpmnProcessSpecMapping", dict[str, Bpm class ProcessInstanceProcessor: _default_script_engine = CustomBpmnScriptEngine() - SERIALIZER_VERSION = "3" wf_spec_converter = BpmnWorkflowSerializer.configure(SPIFF_CONFIG) - _serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION) + _serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SPIFFWORKFLOW_BACKEND_SERIALIZER_VERSION) PROCESS_INSTANCE_ID_KEY = "process_instance_id" VALIDATION_PROCESS_KEY = "validate_only" @@ -496,6 +496,34 @@ class ProcessInstanceProcessor: ), ) from ke + @classmethod + def persist_bpmn_process_dict( + cls, + bpmn_process_dict: dict, + bpmn_definition_to_task_definitions_mappings: dict, + process_instance_model: ProcessInstanceModel, + ) -> None: + cls._add_bpmn_process_definitions( + bpmn_process_dict, + bpmn_definition_to_task_definitions_mappings=bpmn_definition_to_task_definitions_mappings, + process_instance_model=process_instance_model, + force_update=True, + ) + task_service = TaskService( + process_instance=process_instance_model, + serializer=cls._serializer, + bpmn_definition_to_task_definitions_mappings=bpmn_definition_to_task_definitions_mappings, + force_update_definitions=True, + ) + + process_copy = copy.deepcopy(bpmn_process_dict) + bpmn_process_instance = cls._serializer.from_dict(process_copy) + bpmn_process_instance.script_engine = cls._default_script_engine + for spiff_task in bpmn_process_instance.get_tasks(): + task_service.update_task_model_with_spiff_task(spiff_task) + task_service.save_objects_to_database() + db.session.commit() + @classmethod def get_process_model_and_subprocesses( cls, @@ -913,9 +941,11 @@ class ProcessInstanceProcessor: db.session.add(pim) db.session.commit() + @classmethod def _store_bpmn_process_definition( - self, + cls, process_bpmn_properties: dict, + bpmn_definition_to_task_definitions_mappings: dict, bpmn_process_definition_parent: BpmnProcessDefinitionModel | None = None, store_bpmn_definition_mappings: bool = False, full_bpmn_spec_dict: dict | None = None, @@ -943,9 +973,10 @@ class ProcessInstanceProcessor: bpmn_name=process_bpmn_name, properties_json=process_bpmn_properties, ) + process_bpmn_properties["task_specs"] = task_specs db.session.add(bpmn_process_definition) - self._update_bpmn_definition_mappings( - self.bpmn_definition_to_task_definitions_mappings, + cls._update_bpmn_definition_mappings( + bpmn_definition_to_task_definitions_mappings, bpmn_process_definition.bpmn_identifier, bpmn_process_definition=bpmn_process_definition, ) @@ -960,23 +991,23 @@ class ProcessInstanceProcessor: ) db.session.add(task_definition) if store_bpmn_definition_mappings: - self._update_bpmn_definition_mappings( - self.bpmn_definition_to_task_definitions_mappings, + cls._update_bpmn_definition_mappings( + bpmn_definition_to_task_definitions_mappings, process_bpmn_identifier, task_definition=task_definition, ) elif store_bpmn_definition_mappings: # this should only ever happen when new process instances use a pre-existing bpmn process definitions # otherwise this should get populated on processor initialization - self._update_bpmn_definition_mappings( - self.bpmn_definition_to_task_definitions_mappings, + cls._update_bpmn_definition_mappings( + bpmn_definition_to_task_definitions_mappings, process_bpmn_identifier, bpmn_process_definition=bpmn_process_definition, ) task_definitions = TaskDefinitionModel.query.filter_by(bpmn_process_definition_id=bpmn_process_definition.id).all() for task_definition in task_definitions: - self._update_bpmn_definition_mappings( - self.bpmn_definition_to_task_definitions_mappings, + cls._update_bpmn_definition_mappings( + bpmn_definition_to_task_definitions_mappings, process_bpmn_identifier, task_definition=task_definition, ) @@ -994,35 +1025,43 @@ class ProcessInstanceProcessor: db.session.add(bpmn_process_definition_relationship) return bpmn_process_definition - def _add_bpmn_process_definitions(self) -> None: + @classmethod + def _add_bpmn_process_definitions( + cls, + bpmn_process_dict: dict, + bpmn_definition_to_task_definitions_mappings: dict, + process_instance_model: ProcessInstanceModel, + force_update: bool = False, + ) -> None: """Adds serialized_bpmn_definition records to the db session. Expects the calling method to commit it. """ - if self.process_instance_model.spiffworkflow_fully_initialized(): + if force_update is False and process_instance_model.spiffworkflow_fully_initialized(): return None - bpmn_dict = self.serialize() - bpmn_dict_keys = ("spec", "subprocess_specs", "serializer_version") + bpmn_dict_keys = BpmnProcessDefinitionModel.keys_for_full_process_model_hash() bpmn_spec_dict = {} - for bpmn_key in bpmn_dict.keys(): + for bpmn_key in bpmn_process_dict.keys(): if bpmn_key in bpmn_dict_keys: - bpmn_spec_dict[bpmn_key] = bpmn_dict[bpmn_key] + bpmn_spec_dict[bpmn_key] = bpmn_process_dict[bpmn_key] # store only if mappings is currently empty. this also would mean this is a new instance that has never saved before - store_bpmn_definition_mappings = not self.bpmn_definition_to_task_definitions_mappings - bpmn_process_definition_parent = self._store_bpmn_process_definition( + store_bpmn_definition_mappings = not bpmn_definition_to_task_definitions_mappings + bpmn_process_definition_parent = cls._store_bpmn_process_definition( bpmn_spec_dict["spec"], + bpmn_definition_to_task_definitions_mappings=bpmn_definition_to_task_definitions_mappings, store_bpmn_definition_mappings=store_bpmn_definition_mappings, full_bpmn_spec_dict=bpmn_spec_dict, ) for process_bpmn_properties in bpmn_spec_dict["subprocess_specs"].values(): - self._store_bpmn_process_definition( + cls._store_bpmn_process_definition( process_bpmn_properties, - bpmn_process_definition_parent, + bpmn_definition_to_task_definitions_mappings=bpmn_definition_to_task_definitions_mappings, + bpmn_process_definition_parent=bpmn_process_definition_parent, store_bpmn_definition_mappings=store_bpmn_definition_mappings, ) - self.process_instance_model.bpmn_process_definition = bpmn_process_definition_parent + process_instance_model.bpmn_process_definition = bpmn_process_definition_parent # # builds and caches the element units for the parent bpmn process defintion. these @@ -1046,7 +1085,7 @@ class ProcessInstanceProcessor: def save(self) -> None: """Saves the current state of this processor to the database.""" - self.process_instance_model.spiff_serializer_version = self.SERIALIZER_VERSION + self.process_instance_model.spiff_serializer_version = SPIFFWORKFLOW_BACKEND_SERIALIZER_VERSION self.process_instance_model.status = self.get_status().value current_app.logger.debug( f"the_status: {self.process_instance_model.status} for instance {self.process_instance_model.id}" @@ -1457,7 +1496,11 @@ class ProcessInstanceProcessor: execution_strategy_name: str | None = None, execution_strategy: ExecutionStrategy | None = None, ) -> TaskRunnability: - self._add_bpmn_process_definitions() + self._add_bpmn_process_definitions( + self.serialize(), + bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings, + process_instance_model=self.process_instance_model, + ) task_model_delegate = TaskModelSavingDelegate( serializer=self._serializer, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py index 88542c672..c09058071 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py @@ -110,11 +110,17 @@ class TaskService: serializer: BpmnWorkflowSerializer, bpmn_definition_to_task_definitions_mappings: dict, run_started_at: float | None = None, + force_update_definitions: bool = False, ) -> None: self.process_instance = process_instance self.bpmn_definition_to_task_definitions_mappings = bpmn_definition_to_task_definitions_mappings self.serializer = serializer + # this updates the definition ids for both tasks and bpmn_processes when they are updated + # in case definitions were changed for the same instances. + # this is currently only used when importing a process instance from bpmn json or when running data migrations. + self.force_update_definitions = force_update_definitions + self.bpmn_processes: dict[str, BpmnProcessModel] = {} self.task_models: dict[str, TaskModel] = {} self.json_data_dicts: dict[str, JsonDataDict] = {} @@ -229,6 +235,12 @@ class TaskService: ) self.process_instance_events[task_model.guid] = process_instance_event + if self.force_update_definitions is True: + task_definition = self.bpmn_definition_to_task_definitions_mappings[spiff_task.workflow.spec.name][ + spiff_task.task_spec.name + ] + task_model.task_definition_id = task_definition.id + self.update_bpmn_process(spiff_task.workflow, bpmn_process) return task_model @@ -252,6 +264,12 @@ class TaskService: direct_parent_bpmn_process = BpmnProcessModel.query.filter_by(id=bpmn_process.direct_parent_process_id).first() self.update_bpmn_process(spiff_workflow.parent_workflow, direct_parent_bpmn_process) + if self.force_update_definitions is True: + bpmn_process_definition = self.bpmn_definition_to_task_definitions_mappings[spiff_workflow.spec.name][ + "bpmn_process_definition" + ] + bpmn_process.bpmn_process_definition_id = bpmn_process_definition.id + def update_task_model( self, task_model: TaskModel, @@ -294,17 +312,16 @@ class TaskService: bpmn_process = self.task_bpmn_process( spiff_task, ) - task_model = TaskModel.query.filter_by(guid=spiff_task_guid).first() - if task_model is None: - task_definition = self.bpmn_definition_to_task_definitions_mappings[spiff_task.workflow.spec.name][ - spiff_task.task_spec.name - ] - task_model = TaskModel( - guid=spiff_task_guid, - bpmn_process_id=bpmn_process.id, - process_instance_id=self.process_instance.id, - task_definition_id=task_definition.id, - ) + task_definition = self.bpmn_definition_to_task_definitions_mappings[spiff_task.workflow.spec.name][ + spiff_task.task_spec.name + ] + task_model = TaskModel( + guid=spiff_task_guid, + bpmn_process_id=bpmn_process.id, + process_instance_id=self.process_instance.id, + task_definition_id=task_definition.id, + ) + return (bpmn_process, task_model) def task_bpmn_process( diff --git a/spiffworkflow-backend/tests/data/service-task-with-data-obj/main.bpmn b/spiffworkflow-backend/tests/data/service-task-with-data-obj/main.bpmn new file mode 100644 index 000000000..c2f1cf629 --- /dev/null +++ b/spiffworkflow-backend/tests/data/service-task-with-data-obj/main.bpmn @@ -0,0 +1,80 @@ + + + + + Flow_0j3unyw + + + + Flow_1dbi4yf + + + + Flow_0uoa0st + Flow_1dbi4yf + + + + + + + + + + + + + + + + + + Flow_0j3unyw + Flow_0uoa0st + + DataObjectReference_0qfbjfj + + top_level_data_object = "a" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/service-task-with-data-obj/without-service-task.bpmn b/spiffworkflow-backend/tests/data/service-task-with-data-obj/without-service-task.bpmn new file mode 100644 index 000000000..3e12ce6ef --- /dev/null +++ b/spiffworkflow-backend/tests/data/service-task-with-data-obj/without-service-task.bpmn @@ -0,0 +1,163 @@ + + + + + Flow_18zaszw + + + + The process instance completed successfully. + + Flow_02mnp5n + + + + Flow_18zaszw + Flow_1dbpkjb + + DataObjectReference_11gskr0 + + sub_level_data_object = "b" + + + + + + + Flow_1dbpkjb + Flow_15y3o98 + + + + Flow_0zr28rt + Flow_02mnp5n + + DataObjectReference_06uyy2z + + sub_level_data_object_two = 'c' + + + + + + Flow_15y3o98 + Flow_0zr28rt + + Flow_1ekcyuv + + + + Flow_1p3g9vw + + + + + + Flow_1ekcyuv + Flow_1p3g9vw + + DataObjectReference_1v83y6h + + sub_level_data_object_three = 'd' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/files/bpmn_process_instance_data_objects_version_3.json b/spiffworkflow-backend/tests/files/bpmn_process_instance_data_objects_version_3.json new file mode 100644 index 000000000..ee23bf449 --- /dev/null +++ b/spiffworkflow-backend/tests/files/bpmn_process_instance_data_objects_version_3.json @@ -0,0 +1,966 @@ +{ + "data": { + "validate_only": false, + "top_level_data_object": "a", + "sub_level_data_object_two": "c", + "sub_level_data_object_three": "d" + }, + "correlations": {}, + "last_task": "a8052b4d-65ed-4e55-8233-062113ebe18f", + "success": true, + "tasks": { + "098e4fc2-a399-4325-b0a9-76d6c330fbf4": { + "id": "098e4fc2-a399-4325-b0a9-76d6c330fbf4", + "parent": "142bb43d-7d87-4341-acb5-f7762e48d8d3", + "children": [ + "aa3991dd-2e91-4210-89e1-594245a0cf15" + ], + "last_state_change": 1710947480.2315426, + "state": 64, + "task_spec": "StartEvent_1", + "triggered": false, + "internal_data": { + "event_fired": true + }, + "data": {}, + "typename": "Task" + }, + "142bb43d-7d87-4341-acb5-f7762e48d8d3": { + "id": "142bb43d-7d87-4341-acb5-f7762e48d8d3", + "parent": null, + "children": [ + "098e4fc2-a399-4325-b0a9-76d6c330fbf4" + ], + "last_state_change": 1710947480.210416, + "state": 64, + "task_spec": "Start", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "270d76e0-c1fe-4add-b58e-d5a51214a37b": { + "id": "270d76e0-c1fe-4add-b58e-d5a51214a37b", + "parent": "aa3991dd-2e91-4210-89e1-594245a0cf15", + "children": [ + "e1188a09-95be-4b79-9a10-f7c376fa04a0" + ], + "last_state_change": 1710950132.28626, + "state": 64, + "task_spec": "top_call_activity", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "973db925-12b3-4f45-95fe-53215db8929d": { + "id": "973db925-12b3-4f45-95fe-53215db8929d", + "parent": "e1188a09-95be-4b79-9a10-f7c376fa04a0", + "children": [ + "a8052b4d-65ed-4e55-8233-062113ebe18f" + ], + "last_state_change": 1710950132.2983754, + "state": 64, + "task_spec": "Process_top_level.EndJoin", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "a8052b4d-65ed-4e55-8233-062113ebe18f": { + "id": "a8052b4d-65ed-4e55-8233-062113ebe18f", + "parent": "973db925-12b3-4f45-95fe-53215db8929d", + "children": [], + "last_state_change": 1710950132.3121655, + "state": 64, + "task_spec": "End", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "aa3991dd-2e91-4210-89e1-594245a0cf15": { + "id": "aa3991dd-2e91-4210-89e1-594245a0cf15", + "parent": "098e4fc2-a399-4325-b0a9-76d6c330fbf4", + "children": [ + "270d76e0-c1fe-4add-b58e-d5a51214a37b" + ], + "last_state_change": 1710947480.247028, + "state": 64, + "task_spec": "top_script_task", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "e1188a09-95be-4b79-9a10-f7c376fa04a0": { + "id": "e1188a09-95be-4b79-9a10-f7c376fa04a0", + "parent": "270d76e0-c1fe-4add-b58e-d5a51214a37b", + "children": [ + "973db925-12b3-4f45-95fe-53215db8929d" + ], + "last_state_change": 1710950132.2913136, + "state": 64, + "task_spec": "Event_1swh8gs", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + } + }, + "root": "142bb43d-7d87-4341-acb5-f7762e48d8d3", + "spec": { + "name": "Process_top_level", + "description": "Top Level Process", + "file": "main.bpmn", + "task_specs": { + "End": { + "name": "End", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "Process_top_level.EndJoin" + ], + "outputs": [], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "SimpleBpmnTask" + }, + "Event_1swh8gs": { + "name": "Event_1swh8gs", + "description": "Default End Event", + "manual": false, + "lookahead": 2, + "inputs": [ + "top_call_activity" + ], + "outputs": [ + "Process_top_level.EndJoin" + ], + "bpmn_id": "Event_1swh8gs", + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "event_definition": { + "description": "Default", + "name": null, + "typename": "NoneEventDefinition" + }, + "typename": "EndEvent", + "extensions": {} + }, + "Process_top_level.EndJoin": { + "name": "Process_top_level.EndJoin", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "Event_1swh8gs" + ], + "outputs": [ + "End" + ], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "_EndJoin" + }, + "Start": { + "name": "Start", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [], + "outputs": [ + "StartEvent_1" + ], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "BpmnStartTask" + }, + "StartEvent_1": { + "name": "StartEvent_1", + "description": "Default Start Event", + "manual": false, + "lookahead": 2, + "inputs": [ + "Start" + ], + "outputs": [ + "top_script_task" + ], + "bpmn_id": "StartEvent_1", + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "event_definition": { + "description": "Default", + "name": null, + "typename": "NoneEventDefinition" + }, + "typename": "StartEvent", + "extensions": {} + }, + "top_call_activity": { + "name": "top_call_activity", + "description": "Call Activity", + "manual": false, + "lookahead": 2, + "inputs": [ + "top_script_task" + ], + "outputs": [ + "Event_1swh8gs" + ], + "bpmn_id": "top_call_activity", + "bpmn_name": "Top Call Activity", + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "prescript": null, + "postscript": null, + "spec": "Process_sub_level", + "typename": "CallActivity", + "extensions": {} + }, + "top_script_task": { + "name": "top_script_task", + "description": "Script Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "StartEvent_1" + ], + "outputs": [ + "top_call_activity" + ], + "bpmn_id": "top_script_task", + "bpmn_name": "Top Script Task", + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [ + { + "bpmn_id": "top_level_data_object", + "bpmn_name": "top_level_data_object", + "category": null, + "typename": "DataObject" + } + ], + "io_specification": null, + "prescript": null, + "postscript": null, + "script": "top_level_data_object = \"a\"", + "typename": "ScriptTask", + "extensions": { + "serviceTaskOperator": { + "name": "http/GetRequestV2", + "parameters": { + "url": { + "type": "str", + "value": "\"http://localhost:7000/v1.0/status\"" + } + }, + "resultVariable": "the_response" + } + } + } + }, + "io_specification": null, + "data_objects": { + "top_level_data_object": { + "bpmn_id": "top_level_data_object", + "bpmn_name": "top_level_data_object", + "category": null, + "typename": "DataObject" + } + }, + "correlation_keys": {}, + "typename": "BpmnProcessSpec" + }, + "subprocess_specs": { + "Process_sub_level": { + "name": "Process_sub_level", + "description": "Process Sub Level", + "file": "without-service-task.bpmn", + "task_specs": { + "call_activity_sub_process": { + "name": "call_activity_sub_process", + "description": "Subprocess", + "manual": false, + "lookahead": 2, + "inputs": [ + "sub_manual_task" + ], + "outputs": [ + "sub_script_task_two" + ], + "bpmn_id": "call_activity_sub_process", + "bpmn_name": "Call Activity Sub Process", + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "prescript": null, + "postscript": null, + "spec": "call_activity_sub_process", + "typename": "SubWorkflowTask", + "extensions": {} + }, + "End": { + "name": "End", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "Process_sub_level.EndJoin" + ], + "outputs": [], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "SimpleBpmnTask" + }, + "EndEvent_1": { + "name": "EndEvent_1", + "description": "Default End Event", + "manual": false, + "lookahead": 2, + "inputs": [ + "sub_script_task_two" + ], + "outputs": [ + "Process_sub_level.EndJoin" + ], + "bpmn_id": "EndEvent_1", + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "event_definition": { + "description": "Default", + "name": null, + "typename": "NoneEventDefinition" + }, + "typename": "EndEvent", + "extensions": { + "instructionsForEndUser": "The process instance completed successfully." + } + }, + "Process_sub_level.EndJoin": { + "name": "Process_sub_level.EndJoin", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "EndEvent_1" + ], + "outputs": [ + "End" + ], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "_EndJoin" + }, + "Start": { + "name": "Start", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [], + "outputs": [ + "StartEvent_1" + ], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "BpmnStartTask" + }, + "StartEvent_1": { + "name": "StartEvent_1", + "description": "Default Start Event", + "manual": false, + "lookahead": 2, + "inputs": [ + "Start" + ], + "outputs": [ + "sub_script_task" + ], + "bpmn_id": "StartEvent_1", + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "event_definition": { + "description": "Default", + "name": null, + "typename": "NoneEventDefinition" + }, + "typename": "StartEvent", + "extensions": {} + }, + "sub_manual_task": { + "name": "sub_manual_task", + "description": "Manual Task", + "manual": true, + "lookahead": 2, + "inputs": [ + "sub_script_task" + ], + "outputs": [ + "call_activity_sub_process" + ], + "bpmn_id": "sub_manual_task", + "bpmn_name": "Sub Manual Task", + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "prescript": null, + "postscript": null, + "typename": "ManualTask", + "extensions": {} + }, + "sub_script_task": { + "name": "sub_script_task", + "description": "Script Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "StartEvent_1" + ], + "outputs": [ + "sub_manual_task" + ], + "bpmn_id": "sub_script_task", + "bpmn_name": "Sub Script Task", + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [ + { + "bpmn_id": "sub_level_data_object", + "bpmn_name": "sub_level_data_object", + "category": null, + "typename": "DataObject" + } + ], + "io_specification": null, + "prescript": null, + "postscript": null, + "script": "sub_level_data_object = \"b\"", + "typename": "ScriptTask", + "extensions": {} + }, + "sub_script_task_two": { + "name": "sub_script_task_two", + "description": "Script Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "call_activity_sub_process" + ], + "outputs": [ + "EndEvent_1" + ], + "bpmn_id": "sub_script_task_two", + "bpmn_name": "Sub Script Task Two", + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [ + { + "bpmn_id": "sub_level_data_object_two", + "bpmn_name": "sub_level_data_object_two", + "category": null, + "typename": "DataObject" + } + ], + "io_specification": null, + "prescript": null, + "postscript": null, + "script": "sub_level_data_object_two = 'c'", + "typename": "ScriptTask", + "extensions": {} + } + }, + "io_specification": null, + "data_objects": { + "sub_level_data_object": { + "bpmn_id": "sub_level_data_object", + "bpmn_name": "sub_level_data_object", + "category": null, + "typename": "DataObject" + }, + "sub_level_data_object_two": { + "bpmn_id": "sub_level_data_object_two", + "bpmn_name": "sub_level_data_object_two", + "category": null, + "typename": "DataObject" + } + }, + "correlation_keys": {}, + "typename": "BpmnProcessSpec" + }, + "call_activity_sub_process": { + "name": "call_activity_sub_process", + "description": "Call Activity Sub Process", + "file": "without-service-task.bpmn", + "task_specs": { + "call_activity_sub_process.EndJoin": { + "name": "call_activity_sub_process.EndJoin", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "Event_00vjfmy" + ], + "outputs": [ + "End" + ], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "_EndJoin" + }, + "End": { + "name": "End", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "call_activity_sub_process.EndJoin" + ], + "outputs": [], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "SimpleBpmnTask" + }, + "Event_00vjfmy": { + "name": "Event_00vjfmy", + "description": "Default End Event", + "manual": false, + "lookahead": 2, + "inputs": [ + "sub_level_sub_process_script_task" + ], + "outputs": [ + "call_activity_sub_process.EndJoin" + ], + "bpmn_id": "Event_00vjfmy", + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "event_definition": { + "description": "Default", + "name": null, + "typename": "NoneEventDefinition" + }, + "typename": "EndEvent", + "extensions": {} + }, + "Event_0yfq3gm": { + "name": "Event_0yfq3gm", + "description": "Default Start Event", + "manual": false, + "lookahead": 2, + "inputs": [ + "Start" + ], + "outputs": [ + "sub_level_sub_process_script_task" + ], + "bpmn_id": "Event_0yfq3gm", + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "event_definition": { + "description": "Default", + "name": null, + "typename": "NoneEventDefinition" + }, + "typename": "StartEvent", + "extensions": {} + }, + "Start": { + "name": "Start", + "description": "BPMN Task", + "manual": false, + "lookahead": 2, + "inputs": [], + "outputs": [ + "Event_0yfq3gm" + ], + "bpmn_id": null, + "bpmn_name": null, + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [], + "io_specification": null, + "typename": "BpmnStartTask" + }, + "sub_level_sub_process_script_task": { + "name": "sub_level_sub_process_script_task", + "description": "Script Task", + "manual": false, + "lookahead": 2, + "inputs": [ + "Event_0yfq3gm" + ], + "outputs": [ + "Event_00vjfmy" + ], + "bpmn_id": "sub_level_sub_process_script_task", + "bpmn_name": "Sub Level Sub Process Script Task", + "lane": null, + "documentation": null, + "data_input_associations": [], + "data_output_associations": [ + { + "bpmn_id": "sub_level_data_object_three", + "bpmn_name": "sub_level_data_object_three", + "category": null, + "typename": "DataObject" + } + ], + "io_specification": null, + "prescript": null, + "postscript": null, + "script": "sub_level_data_object_three = 'd'", + "typename": "ScriptTask", + "extensions": {} + } + }, + "io_specification": null, + "data_objects": { + "sub_level_data_object": { + "bpmn_id": "sub_level_data_object", + "bpmn_name": "sub_level_data_object", + "category": null, + "typename": "DataObject" + }, + "sub_level_data_object_two": { + "bpmn_id": "sub_level_data_object_two", + "bpmn_name": "sub_level_data_object_two", + "category": null, + "typename": "DataObject" + }, + "sub_level_data_object_three": { + "bpmn_id": "sub_level_data_object_three", + "bpmn_name": "sub_level_data_object_three", + "category": null, + "typename": "DataObject" + } + }, + "correlation_keys": {}, + "typename": "BpmnProcessSpec" + } + }, + "subprocesses": { + "270d76e0-c1fe-4add-b58e-d5a51214a37b": { + "data": { + "validate_only": false, + "top_level_data_object": "a", + "sub_level_data_object_two": "c", + "sub_level_data_object_three": "d" + }, + "correlations": {}, + "last_task": "5eb9e777-cfbf-4ef9-8ba8-79fa5d172b7e", + "success": true, + "tasks": { + "0315382d-fdf6-4c27-8d7d-63dddf0b05fb": { + "id": "0315382d-fdf6-4c27-8d7d-63dddf0b05fb", + "parent": "8efb7c04-82d1-459f-b0f8-778782dd7f0e", + "children": [ + "5eb9e777-cfbf-4ef9-8ba8-79fa5d172b7e" + ], + "last_state_change": 1710950132.2467537, + "state": 64, + "task_spec": "Process_sub_level.EndJoin", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "186869f9-6992-4a79-86dc-365c3906dd64": { + "id": "186869f9-6992-4a79-86dc-365c3906dd64", + "parent": null, + "children": [ + "b9762626-24e2-48d8-939a-ce1b17757781" + ], + "last_state_change": 1710947480.247221, + "state": 64, + "task_spec": "Start", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "5eb9e777-cfbf-4ef9-8ba8-79fa5d172b7e": { + "id": "5eb9e777-cfbf-4ef9-8ba8-79fa5d172b7e", + "parent": "0315382d-fdf6-4c27-8d7d-63dddf0b05fb", + "children": [], + "last_state_change": 1710950132.2639363, + "state": 64, + "task_spec": "End", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "688506fc-ab27-4eb2-a1fa-b435dd958561": { + "id": "688506fc-ab27-4eb2-a1fa-b435dd958561", + "parent": "8b51d215-15ab-4e0a-8dfc-e335e685fb52", + "children": [ + "d0c6a2d9-9a43-4ccd-b4e3-ea62872f15ed" + ], + "last_state_change": 1710950131.6774, + "state": 64, + "task_spec": "sub_manual_task", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "8b51d215-15ab-4e0a-8dfc-e335e685fb52": { + "id": "8b51d215-15ab-4e0a-8dfc-e335e685fb52", + "parent": "b9762626-24e2-48d8-939a-ce1b17757781", + "children": [ + "688506fc-ab27-4eb2-a1fa-b435dd958561" + ], + "last_state_change": 1710947480.280872, + "state": 64, + "task_spec": "sub_script_task", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "8efb7c04-82d1-459f-b0f8-778782dd7f0e": { + "id": "8efb7c04-82d1-459f-b0f8-778782dd7f0e", + "parent": "b482d5b3-a8e0-4903-9d48-0dbce70bd682", + "children": [ + "0315382d-fdf6-4c27-8d7d-63dddf0b05fb" + ], + "last_state_change": 1710950132.1689236, + "state": 64, + "task_spec": "EndEvent_1", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "b482d5b3-a8e0-4903-9d48-0dbce70bd682": { + "id": "b482d5b3-a8e0-4903-9d48-0dbce70bd682", + "parent": "d0c6a2d9-9a43-4ccd-b4e3-ea62872f15ed", + "children": [ + "8efb7c04-82d1-459f-b0f8-778782dd7f0e" + ], + "last_state_change": 1710950131.849423, + "state": 64, + "task_spec": "sub_script_task_two", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "b9762626-24e2-48d8-939a-ce1b17757781": { + "id": "b9762626-24e2-48d8-939a-ce1b17757781", + "parent": "186869f9-6992-4a79-86dc-365c3906dd64", + "children": [ + "8b51d215-15ab-4e0a-8dfc-e335e685fb52" + ], + "last_state_change": 1710947480.255076, + "state": 64, + "task_spec": "StartEvent_1", + "triggered": false, + "internal_data": { + "event_fired": true + }, + "data": {}, + "typename": "Task" + }, + "d0c6a2d9-9a43-4ccd-b4e3-ea62872f15ed": { + "id": "d0c6a2d9-9a43-4ccd-b4e3-ea62872f15ed", + "parent": "688506fc-ab27-4eb2-a1fa-b435dd958561", + "children": [ + "b482d5b3-a8e0-4903-9d48-0dbce70bd682" + ], + "last_state_change": 1710950131.8426125, + "state": 64, + "task_spec": "call_activity_sub_process", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + } + }, + "root": "186869f9-6992-4a79-86dc-365c3906dd64", + "parent_task_id": "270d76e0-c1fe-4add-b58e-d5a51214a37b", + "spec": "Process_sub_level", + "typename": "BpmnSubWorkflow" + }, + "d0c6a2d9-9a43-4ccd-b4e3-ea62872f15ed": { + "data": { + "validate_only": false, + "top_level_data_object": "a", + "sub_level_data_object_two": "c", + "sub_level_data_object_three": "d" + }, + "correlations": {}, + "last_task": "af12522c-811b-4258-a569-65890838677f", + "success": true, + "tasks": { + "6e6ad5c3-e701-4b59-8a81-4ed2c63bd0e1": { + "id": "6e6ad5c3-e701-4b59-8a81-4ed2c63bd0e1", + "parent": "b346574d-c50c-4b4b-864c-685803ebf14e", + "children": [ + "af12522c-811b-4258-a569-65890838677f" + ], + "last_state_change": 1710950131.829411, + "state": 64, + "task_spec": "call_activity_sub_process.EndJoin", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "95974b26-58b8-4fc3-a6d1-2158c1ab6de8": { + "id": "95974b26-58b8-4fc3-a6d1-2158c1ab6de8", + "parent": "b22dae80-ce20-4565-983e-e86b98625554", + "children": [ + "b346574d-c50c-4b4b-864c-685803ebf14e" + ], + "last_state_change": 1710950131.8059175, + "state": 64, + "task_spec": "sub_level_sub_process_script_task", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "af12522c-811b-4258-a569-65890838677f": { + "id": "af12522c-811b-4258-a569-65890838677f", + "parent": "6e6ad5c3-e701-4b59-8a81-4ed2c63bd0e1", + "children": [], + "last_state_change": 1710950131.8363433, + "state": 64, + "task_spec": "End", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "b22dae80-ce20-4565-983e-e86b98625554": { + "id": "b22dae80-ce20-4565-983e-e86b98625554", + "parent": "ca86a501-34f8-48fc-b284-cce8e8af058d", + "children": [ + "95974b26-58b8-4fc3-a6d1-2158c1ab6de8" + ], + "last_state_change": 1710950131.7874112, + "state": 64, + "task_spec": "Event_0yfq3gm", + "triggered": false, + "internal_data": { + "event_fired": true + }, + "data": {}, + "typename": "Task" + }, + "b346574d-c50c-4b4b-864c-685803ebf14e": { + "id": "b346574d-c50c-4b4b-864c-685803ebf14e", + "parent": "95974b26-58b8-4fc3-a6d1-2158c1ab6de8", + "children": [ + "6e6ad5c3-e701-4b59-8a81-4ed2c63bd0e1" + ], + "last_state_change": 1710950131.817766, + "state": 64, + "task_spec": "Event_00vjfmy", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + }, + "ca86a501-34f8-48fc-b284-cce8e8af058d": { + "id": "ca86a501-34f8-48fc-b284-cce8e8af058d", + "parent": null, + "children": [ + "b22dae80-ce20-4565-983e-e86b98625554" + ], + "last_state_change": 1710950131.677685, + "state": 64, + "task_spec": "Start", + "triggered": false, + "internal_data": {}, + "data": {}, + "typename": "Task" + } + }, + "root": "ca86a501-34f8-48fc-b284-cce8e8af058d", + "parent_task_id": "d0c6a2d9-9a43-4ccd-b4e3-ea62872f15ed", + "spec": "call_activity_sub_process", + "typename": "BpmnSubWorkflow" + } + }, + "bpmn_events": [], + "typename": "BpmnWorkflow" +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 516883f4b..6d20643ca 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -566,3 +566,20 @@ class BaseTest: yield finally: app.config[config_identifier] = initial_value + + def round_last_state_change(self, bpmn_process_dict: dict | list) -> None: + """Round last state change to the nearest 4 significant digits. + + Works around imprecise floating point values in mysql json columns. + The values between mysql and SpiffWorkflow seem to have minor differences on randomly and since + we do not care about such precision for this field, round it to a value that is more likely to match. + """ + if isinstance(bpmn_process_dict, dict): + for key, value in bpmn_process_dict.items(): + if key == "last_state_change": + bpmn_process_dict[key] = round(value, 4) + elif isinstance(value, dict | list): + self.round_last_state_change(value) + elif isinstance(bpmn_process_dict, list): + for item in bpmn_process_dict: + self.round_last_state_change(item) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_migrator.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_migrator.py index 7c8a279aa..dae3ba750 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_migrator.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_migrator.py @@ -1,13 +1,20 @@ import copy +import json +import os from flask.app import Flask from flask.testing import FlaskClient +from SpiffWorkflow.bpmn.serializer.migration.version_1_3 import update_data_objects # type: ignore from spiffworkflow_backend.data_migrations.process_instance_migrator import ProcessInstanceMigrator from spiffworkflow_backend.data_migrations.version_1_3 import VersionOneThree +from spiffworkflow_backend.data_migrations.version_4 import Version4 +from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.db import db +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor +from sqlalchemy import or_ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec @@ -73,3 +80,75 @@ class TestProcessInstanceMigrator(BaseTest): VersionOneThree().run() task_model = TaskModel.query.filter_by(guid=task_model.guid).first() assert task_model.properties_json["last_state_change"] is not None + + def test_can_run_version_4_migration( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + version_3_json = os.path.join( + app.instance_path, + "..", + "..", + "tests", + "files", + "bpmn_process_instance_data_objects_version_3.json", + ) + with open(version_3_json) as f: + bpmn_process_dict_version_3 = json.loads(f.read()) + bpmn_process_dict_version_4_from_spiff = copy.deepcopy(bpmn_process_dict_version_3) + process_model = load_test_spec( + process_model_id="test_group/service-task-with-data-obj", + process_model_source_directory="service-task-with-data-obj", + ) + process_instance = self.create_process_instance_from_process_model(process_model=process_model) + ProcessInstanceProcessor.persist_bpmn_process_dict( + bpmn_process_dict_version_3, process_instance_model=process_instance, bpmn_definition_to_task_definitions_mappings={} + ) + process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first() + + # ensure data was imported correctly and is in expected state + processor = ProcessInstanceProcessor(process_instance) + bpmn_process_dict_version_3_after_import = processor.serialize() + self.round_last_state_change(bpmn_process_dict_version_3) + self.round_last_state_change(bpmn_process_dict_version_3_after_import) + assert bpmn_process_dict_version_3_after_import == bpmn_process_dict_version_3 + bpmn_process_cache_version_3 = { + "bpmn_process_definition_id": process_instance.bpmn_process_definition_id, + "bpmn_process_id": process_instance.bpmn_process_id, + } + + Version4.run(process_instance) + update_data_objects(bpmn_process_dict_version_4_from_spiff) + process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first() + processor = ProcessInstanceProcessor(process_instance) + bpmn_process_dict_version_4 = processor.serialize() + self.round_last_state_change(bpmn_process_dict_version_4) + self.round_last_state_change(bpmn_process_dict_version_4_from_spiff) + assert bpmn_process_dict_version_4 == bpmn_process_dict_version_4_from_spiff + + bpmn_process_cache_version_4 = { + "bpmn_process_definition_id": process_instance.bpmn_process_definition_id, + "bpmn_process_id": process_instance.bpmn_process_id, + } + assert ( + bpmn_process_cache_version_4["bpmn_process_definition_id"] + != bpmn_process_cache_version_3["bpmn_process_definition_id"] + ) + assert bpmn_process_cache_version_4["bpmn_process_id"] == bpmn_process_cache_version_3["bpmn_process_id"] + assert ( + process_instance.bpmn_process.bpmn_process_definition_id == bpmn_process_cache_version_4["bpmn_process_definition_id"] + ) + + bpmn_processes = BpmnProcessModel.query.filter( + or_( + BpmnProcessModel.id == process_instance.bpmn_process_id, + BpmnProcessModel.top_level_process_id == process_instance.bpmn_process_id, + ) + ).all() + assert len(bpmn_processes) == 3 + + for bpmn_process in bpmn_processes: + for task_model in bpmn_process.tasks: + assert task_model.task_definition.bpmn_process_definition_id == bpmn_process.bpmn_process_definition_id diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index 0bd50685e..7ec15a291 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -10,6 +10,7 @@ from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskEr from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel @@ -904,3 +905,69 @@ class TestProcessInstanceProcessor(BaseTest): db.session.delete(process_instance) db.session.commit() + + def test_can_persist_given_bpmn_process_dict_when_imported_from_scratch( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + process_model = load_test_spec( + process_model_id="test_group/service-task-with-data-obj", + process_model_source_directory="service-task-with-data-obj", + ) + process_instance = self.create_process_instance_from_process_model(process_model=process_model) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + + bpmn_process_dict_initial = processor.serialize() + + # clear the database so we know the import is all new + meta = db.metadata + db.session.execute(db.update(BpmnProcessModel).values(top_level_process_id=None)) + db.session.execute(db.update(BpmnProcessModel).values(direct_parent_process_id=None)) + for table in reversed(meta.sorted_tables): + db.session.execute(table.delete()) + db.session.commit() + # ensure everything is removed from the sqlalchemy cache when we clear the database + # otherwise it gets autoflush errors + db.session.expunge_all() + + process_instance = self.create_process_instance_from_process_model(process_model=process_model) + assert process_instance.bpmn_process_definition_id is None + + ProcessInstanceProcessor.persist_bpmn_process_dict( + bpmn_process_dict_initial, process_instance_model=process_instance, bpmn_definition_to_task_definitions_mappings={} + ) + processor = ProcessInstanceProcessor(process_instance) + bpmn_process_dict_after = processor.serialize() + self.round_last_state_change(bpmn_process_dict_after) + self.round_last_state_change(bpmn_process_dict_initial) + + assert bpmn_process_dict_after == bpmn_process_dict_initial + + def test_can_persist_given_bpmn_process_dict_when_loaded_before( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + process_model = load_test_spec( + process_model_id="test_group/service-task-with-data-obj", + process_model_source_directory="service-task-with-data-obj", + ) + process_instance = self.create_process_instance_from_process_model(process_model=process_model) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + + bpmn_process_dict_initial = processor.serialize() + + ProcessInstanceProcessor.persist_bpmn_process_dict( + bpmn_process_dict_initial, process_instance_model=process_instance, bpmn_definition_to_task_definitions_mappings={} + ) + processor = ProcessInstanceProcessor(process_instance) + bpmn_process_dict_after = processor.serialize() + self.round_last_state_change(bpmn_process_dict_after) + self.round_last_state_change(bpmn_process_dict_initial) + + assert bpmn_process_dict_after == bpmn_process_dict_initial diff --git a/spiffworkflow-frontend/src/routes/BaseRoutes.tsx b/spiffworkflow-frontend/src/routes/BaseRoutes.tsx index 80a4f15d7..3422f17ef 100644 --- a/spiffworkflow-frontend/src/routes/BaseRoutes.tsx +++ b/spiffworkflow-frontend/src/routes/BaseRoutes.tsx @@ -75,10 +75,12 @@ export default function BaseRoutes({ extensionUxElements }: OwnProps) { const style = { margin: '50px 0 50px 50px' }; return ( - +
+ +
); }