mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-01-13 02:54:27 +00:00
Pi migration (#1892)
* some initial code to migrate a process instance w/ burnettk essweine * the migration test is working now w/ burnettk essweine * use the persist method from the pi migration method w/ burnettk * updated spiffworkflow w/ burnettk * added api to migrate a process instance w/ burnettk * fixed tests w/ burnettk * added api to check if a process instance can be migrated w/ burnettk * return error if pi is not suspended when attempting to migrate w/ burnettk * return error if pi is not suspended when attempting to migrate w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com> Co-authored-by: Kevin Burnett <18027+burnettk@users.noreply.github.com>
This commit is contained in:
parent
738446147e
commit
ec21ffb735
31
spiffworkflow-backend/poetry.lock
generated
31
spiffworkflow-backend/poetry.lock
generated
@ -1789,8 +1789,6 @@ 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"},
|
||||
@ -2115,7 +2113,6 @@ 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"},
|
||||
@ -2492,51 +2489,37 @@ 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-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"},
|
||||
{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_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-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"},
|
||||
{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_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-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"},
|
||||
{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_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"},
|
||||
@ -2850,7 +2833,7 @@ doc = ["sphinx", "sphinx_rtd_theme"]
|
||||
type = "git"
|
||||
url = "https://github.com/sartography/SpiffWorkflow"
|
||||
reference = "main"
|
||||
resolved_reference = "7e15a0b68f926161bfa88e1306f3145ee028fad4"
|
||||
resolved_reference = "a7ddab83831eb98371a26d94ecc6978287b965c5"
|
||||
|
||||
[[package]]
|
||||
name = "spiffworkflow-connector-command"
|
||||
|
@ -1330,6 +1330,39 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-instances/{modified_process_model_identifier}/{process_instance_id}/check-can-migrate:
|
||||
parameters:
|
||||
- name: modified_process_model_identifier
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of an existing process model
|
||||
schema:
|
||||
type: string
|
||||
- name: process_instance_id
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of an existing process instance.
|
||||
schema:
|
||||
type: integer
|
||||
get:
|
||||
tags:
|
||||
- Process Instances
|
||||
operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_check_can_migrate
|
||||
summary: Checks if a given process instance can be migrated to the newest version of the process model.
|
||||
responses:
|
||||
"200":
|
||||
description: The result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
can_migrate:
|
||||
type: boolean
|
||||
description: True if it can migrate and false if not.
|
||||
process_instance_id:
|
||||
type: integer
|
||||
description: Process instance id.
|
||||
|
||||
/process-instances/{modified_process_model_identifier}/{process_instance_id}/run:
|
||||
parameters:
|
||||
- name: process_instance_id
|
||||
@ -1462,6 +1495,33 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-instance-migrate/{modified_process_model_identifier}/{process_instance_id}:
|
||||
parameters:
|
||||
- name: modified_process_model_identifier
|
||||
in: path
|
||||
required: true
|
||||
description: The modified process model id
|
||||
schema:
|
||||
type: string
|
||||
- name: process_instance_id
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of an existing process instance.
|
||||
schema:
|
||||
type: integer
|
||||
post:
|
||||
operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_migrate
|
||||
summary: Migrate a process instance to the new version of its process model.
|
||||
tags:
|
||||
- Process Instances
|
||||
responses:
|
||||
"200":
|
||||
description: Empty ok true response on successful reset.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-instances/reports:
|
||||
parameters:
|
||||
- name: page
|
||||
|
@ -6,7 +6,12 @@ class RefreshTokenStorageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# These could be either 'id' OR 'access' tokens and we can't always know which
|
||||
class ProcessInstanceMigrationNotSafeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProcessInstanceMigrationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TokenExpiredError(Exception):
|
||||
|
@ -16,6 +16,7 @@ from spiffworkflow_backend.models.user import UserModel
|
||||
class ProcessInstanceEventType(SpiffEnum):
|
||||
process_instance_error = "process_instance_error"
|
||||
process_instance_force_run = "process_instance_force_run"
|
||||
process_instance_migrated = "process_instance_migrated"
|
||||
process_instance_resumed = "process_instance_resumed"
|
||||
process_instance_rewound_to_task = "process_instance_rewound_to_task"
|
||||
process_instance_suspended = "process_instance_suspended"
|
||||
|
@ -1,3 +1,4 @@
|
||||
from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationError, ProcessInstanceMigrationNotSafeError
|
||||
from spiffworkflow_backend.helpers.spiff_enum import ProcessInstanceExecutionMode
|
||||
|
||||
# black and ruff are in competition with each other in import formatting so ignore ruff
|
||||
@ -546,6 +547,36 @@ def process_instance_reset(
|
||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
def process_instance_check_can_migrate(
|
||||
process_instance_id: int,
|
||||
modified_process_model_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
can_migrate = True
|
||||
try:
|
||||
ProcessInstanceService.check_process_instance_can_be_migrated(process_instance)
|
||||
except ProcessInstanceMigrationNotSafeError:
|
||||
can_migrate = False
|
||||
return Response(
|
||||
json.dumps({"can_migrate": can_migrate, "process_instance_id": process_instance.id}),
|
||||
status=200,
|
||||
mimetype="application/json",
|
||||
)
|
||||
|
||||
|
||||
def process_instance_migrate(
|
||||
process_instance_id: int,
|
||||
modified_process_model_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
if process_instance.status != "suspended":
|
||||
raise ProcessInstanceMigrationError(
|
||||
f"The process instance needs to be suspended to migrate it. It is currently: {process_instance.status}"
|
||||
)
|
||||
ProcessInstanceService.migrate_process_instance_to_newest_model_version(process_instance, user=g.user)
|
||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
def process_instance_find_by_id(
|
||||
process_instance_id: int,
|
||||
) -> flask.wrappers.Response:
|
||||
|
@ -70,6 +70,7 @@ PATH_SEGMENTS_FOR_PERMISSION_ALL = [
|
||||
},
|
||||
{"path": "/process-data", "relevant_permissions": ["read"]},
|
||||
{"path": "/process-data-file-download", "relevant_permissions": ["read"]},
|
||||
{"path": "/process-instance-migrate", "relevant_permissions": ["create"]},
|
||||
{"path": "/process-instance-suspend", "relevant_permissions": ["create"]},
|
||||
{"path": "/process-instance-terminate", "relevant_permissions": ["create"]},
|
||||
{"path": "/process-model-natural-language", "relevant_permissions": ["create"]},
|
||||
@ -637,7 +638,7 @@ class AuthorizationService:
|
||||
def set_support_permissions(cls) -> list[PermissionToAssign]:
|
||||
"""Just like elevated permissions minus access to secrets."""
|
||||
permissions_to_assign = cls.set_basic_permissions()
|
||||
for process_instance_action in ["resume", "terminate", "suspend", "reset"]:
|
||||
for process_instance_action in ["migrate", "resume", "terminate", "suspend", "reset"]:
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission="create", target_uri=f"/process-instance-{process_instance_action}/*")
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ from typing import Any
|
||||
from typing import NewType
|
||||
from typing import TypedDict
|
||||
from uuid import UUID
|
||||
from uuid import uuid4
|
||||
|
||||
import dateparser
|
||||
import pytz
|
||||
@ -36,6 +37,7 @@ from SpiffWorkflow.bpmn.serializer.default.task_spec import EventConverter # ty
|
||||
from SpiffWorkflow.bpmn.serializer.helpers.registry import DefaultRegistry # type: ignore
|
||||
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
|
||||
from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec # type: ignore
|
||||
from SpiffWorkflow.bpmn.util.diff import WorkflowDiff # type: ignore
|
||||
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
|
||||
from SpiffWorkflow.exceptions import WorkflowException # type: ignore
|
||||
from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore
|
||||
@ -414,6 +416,7 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
||||
|
||||
|
||||
IdToBpmnProcessSpecMapping = NewType("IdToBpmnProcessSpecMapping", dict[str, BpmnProcessSpec])
|
||||
SubprocessUuidToWorkflowDiffMapping = NewType("SubprocessUuidToWorkflowDiffMapping", dict[UUID, WorkflowDiff])
|
||||
|
||||
|
||||
class ProcessInstanceProcessor:
|
||||
@ -470,7 +473,7 @@ class ProcessInstanceProcessor:
|
||||
self.bpmn_subprocess_mapping: dict[str, BpmnProcessModel] = {}
|
||||
|
||||
# this caches the bpmn_process_definition_identifier and task_identifier back to the bpmn_process_id
|
||||
# in the database. This is to cut down on database queries while adding new tasks to the database.
|
||||
# intthe database. This is to cut down on database queries while adding new tasks to the database.
|
||||
# Structure:
|
||||
# { "[[BPMN_PROCESS_DEFINITION_IDENTIFIER]]": {
|
||||
# "[[TASK_IDENTIFIER]]": [[TASK_DEFINITION]],
|
||||
@ -524,6 +527,7 @@ class ProcessInstanceProcessor:
|
||||
bpmn_definition_to_task_definitions_mappings: dict,
|
||||
process_instance_model: ProcessInstanceModel,
|
||||
store_process_instance_events: bool = True,
|
||||
bpmn_process_instance: BpmnWorkflow | None = None,
|
||||
) -> None:
|
||||
cls._add_bpmn_process_definitions(
|
||||
bpmn_process_dict,
|
||||
@ -531,7 +535,10 @@ class ProcessInstanceProcessor:
|
||||
process_instance_model=process_instance_model,
|
||||
force_update=True,
|
||||
)
|
||||
bpmn_process_instance = cls.initialize_bpmn_process_instance(bpmn_process_dict)
|
||||
|
||||
if bpmn_process_instance is None:
|
||||
bpmn_process_instance = cls.initialize_bpmn_process_instance(bpmn_process_dict)
|
||||
|
||||
task_model_mapping, bpmn_subprocess_mapping = cls.get_db_mappings_from_bpmn_process_dict(bpmn_process_dict)
|
||||
|
||||
task_service = TaskService(
|
||||
@ -1338,6 +1345,42 @@ class ProcessInstanceProcessor:
|
||||
processor.save()
|
||||
processor.suspend()
|
||||
|
||||
@classmethod
|
||||
def update_guids_on_tasks(cls, bpmn_process_instance_dict: dict) -> None:
|
||||
# old -> new
|
||||
guid_map = {}
|
||||
|
||||
def get_guid_map(proc_dict: dict) -> None:
|
||||
for guid in proc_dict["tasks"].keys():
|
||||
guid_map[guid] = str(uuid4())
|
||||
|
||||
def update_guids(proc_dict: dict) -> None:
|
||||
new_tasks = {}
|
||||
for task_guid, task_dict in proc_dict["tasks"].items():
|
||||
new_guid = guid_map[task_guid]
|
||||
new_tasks[new_guid] = task_dict
|
||||
if task_dict["parent"] is not None:
|
||||
new_tasks[new_guid]["parent"] = guid_map[task_dict["parent"]]
|
||||
new_children_guids = [guid_map[cg] for cg in task_dict["children"]]
|
||||
new_tasks[new_guid]["children"] = new_children_guids
|
||||
new_tasks[new_guid]["id"] = guid_map[task_dict["id"]]
|
||||
proc_dict["tasks"] = new_tasks
|
||||
proc_dict["root"] = guid_map[proc_dict["root"]]
|
||||
proc_dict["last_task"] = guid_map[proc_dict["last_task"]]
|
||||
|
||||
get_guid_map(bpmn_process_instance_dict)
|
||||
for subproc_dict in bpmn_process_instance_dict["subprocesses"].values():
|
||||
get_guid_map(subproc_dict)
|
||||
|
||||
update_guids(bpmn_process_instance_dict)
|
||||
new_subprocesses = {}
|
||||
for subproc_guid, subproc_dict in bpmn_process_instance_dict["subprocesses"].items():
|
||||
new_guid = guid_map[subproc_guid]
|
||||
new_subprocesses[new_guid] = subproc_dict
|
||||
new_subprocesses[new_guid]["parent_task_id"] = guid_map[subproc_dict["parent_task_id"]]
|
||||
update_guids(new_subprocesses[new_guid])
|
||||
bpmn_process_instance_dict["subprocesses"] = new_subprocesses
|
||||
|
||||
@staticmethod
|
||||
def get_parser() -> MyCustomParser:
|
||||
parser = MyCustomParser()
|
||||
|
@ -7,14 +7,20 @@ from datetime import datetime
|
||||
from datetime import timezone
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
from uuid import UUID
|
||||
|
||||
import sentry_sdk
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec # type: ignore
|
||||
from SpiffWorkflow.bpmn.specs.control import BoundaryEventSplit # type: ignore
|
||||
from SpiffWorkflow.bpmn.specs.defaults import BoundaryEvent # type: ignore
|
||||
from SpiffWorkflow.bpmn.specs.event_definitions.timer import TimerEventDefinition # type: ignore
|
||||
from SpiffWorkflow.bpmn.util import PendingBpmnEvent # type: ignore
|
||||
from SpiffWorkflow.bpmn.util.diff import WorkflowDiff # type: ignore
|
||||
from SpiffWorkflow.bpmn.util.diff import diff_workflow
|
||||
from SpiffWorkflow.bpmn.util.diff import filter_tasks
|
||||
from SpiffWorkflow.bpmn.util.diff import migrate_workflow
|
||||
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
||||
from SpiffWorkflow.util.task import TaskState # type: ignore
|
||||
@ -27,6 +33,7 @@ from spiffworkflow_backend.data_migrations.process_instance_migrator import Proc
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedError
|
||||
from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError
|
||||
from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationNotSafeError
|
||||
from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskError
|
||||
from spiffworkflow_backend.helpers.spiff_enum import ProcessInstanceExecutionMode
|
||||
from spiffworkflow_backend.models.db import db
|
||||
@ -41,6 +48,7 @@ from spiffworkflow_backend.models.process_instance_file_data import ProcessInsta
|
||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||
from spiffworkflow_backend.models.process_model_cycle import ProcessModelCycleModel
|
||||
from spiffworkflow_backend.models.task import Task
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
||||
@ -48,7 +56,9 @@ from spiffworkflow_backend.services.git_service import GitCommandError
|
||||
from spiffworkflow_backend.services.git_service import GitService
|
||||
from spiffworkflow_backend.services.jinja_service import JinjaService
|
||||
from spiffworkflow_backend.services.process_instance_processor import CustomBpmnScriptEngine
|
||||
from spiffworkflow_backend.services.process_instance_processor import IdToBpmnProcessSpecMapping
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_processor import SubprocessUuidToWorkflowDiffMapping
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsNotEnqueuedError
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceQueueService
|
||||
@ -134,6 +144,82 @@ class ProcessInstanceService:
|
||||
ProcessInstanceQueueService.enqueue_new_process_instance(process_instance_model, run_at_in_seconds)
|
||||
return (process_instance_model, start_configuration)
|
||||
|
||||
@classmethod
|
||||
def check_process_instance_can_be_migrated(
|
||||
cls, process_instance: ProcessInstanceModel
|
||||
) -> tuple[
|
||||
ProcessInstanceProcessor, BpmnProcessSpec, IdToBpmnProcessSpecMapping, WorkflowDiff, SubprocessUuidToWorkflowDiffMapping
|
||||
]:
|
||||
(target_bpmn_process_spec, target_subprocess_specs) = ProcessInstanceProcessor.get_process_model_and_subprocesses(
|
||||
process_instance.process_model_identifier,
|
||||
)
|
||||
processor = ProcessInstanceProcessor(
|
||||
process_instance, include_task_data_for_completed_tasks=True, include_completed_subprocesses=True
|
||||
)
|
||||
|
||||
# tasks that were in the old workflow and are in the new one as well
|
||||
top_level_bpmn_process_diff, subprocesses_diffs = diff_workflow(
|
||||
processor._serializer.registry, processor.bpmn_process_instance, target_bpmn_process_spec, target_subprocess_specs
|
||||
)
|
||||
if not cls.can_migrate(top_level_bpmn_process_diff, subprocesses_diffs):
|
||||
raise ProcessInstanceMigrationNotSafeError(
|
||||
f"It is not safe to migrate process instance {process_instance.id} to "
|
||||
f"new version of '{process_instance.process_model_identifier}'"
|
||||
)
|
||||
return (
|
||||
processor,
|
||||
target_bpmn_process_spec,
|
||||
target_subprocess_specs,
|
||||
top_level_bpmn_process_diff,
|
||||
subprocesses_diffs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def migrate_process_instance_to_newest_model_version(
|
||||
cls, process_instance: ProcessInstanceModel, user: UserModel, preserve_old_process_instance: bool = False
|
||||
) -> None:
|
||||
(
|
||||
processor,
|
||||
target_bpmn_process_spec,
|
||||
target_subprocess_specs,
|
||||
top_level_bpmn_process_diff,
|
||||
subprocesses_diffs,
|
||||
) = cls.check_process_instance_can_be_migrated(process_instance)
|
||||
ProcessInstanceTmpService.add_event_to_process_instance(
|
||||
process_instance, ProcessInstanceEventType.process_instance_rewound_to_task.value
|
||||
)
|
||||
migrate_workflow(top_level_bpmn_process_diff, processor.bpmn_process_instance, target_bpmn_process_spec)
|
||||
for sp_id, sp in processor.bpmn_process_instance.subprocesses.items():
|
||||
migrate_workflow(subprocesses_diffs[sp_id], sp, target_subprocess_specs.get(sp.spec.name))
|
||||
processor.bpmn_process_instance.subprocess_specs = target_subprocess_specs
|
||||
|
||||
if preserve_old_process_instance:
|
||||
# TODO: write tests for this code path - no one has a requirement for it yet
|
||||
bpmn_process_dict = processor.serialize()
|
||||
ProcessInstanceProcessor.update_guids_on_tasks(bpmn_process_dict)
|
||||
new_process_instance, _ = cls.create_process_instance_from_process_model_identifier(
|
||||
process_instance.process_model_identifier, user
|
||||
)
|
||||
ProcessInstanceProcessor.persist_bpmn_process_dict(
|
||||
bpmn_process_dict, bpmn_definition_to_task_definitions_mappings={}, process_instance_model=new_process_instance
|
||||
)
|
||||
else:
|
||||
future_tasks = TaskModel.query.filter(
|
||||
TaskModel.process_instance_id == process_instance.id,
|
||||
TaskModel.state.in_(["FUTURE", "MAYBE", "LIKELY"]), # type: ignore
|
||||
).all()
|
||||
for ft in future_tasks:
|
||||
db.session.delete(ft)
|
||||
db.session.commit()
|
||||
|
||||
bpmn_process_dict = processor.serialize()
|
||||
ProcessInstanceProcessor.persist_bpmn_process_dict(
|
||||
bpmn_process_dict,
|
||||
bpmn_definition_to_task_definitions_mappings={},
|
||||
process_instance_model=process_instance,
|
||||
bpmn_process_instance=processor.bpmn_process_instance,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_process_instance_from_process_model_identifier(
|
||||
cls,
|
||||
@ -642,3 +728,15 @@ class ProcessInstanceService:
|
||||
) from e
|
||||
raise e
|
||||
return processor
|
||||
|
||||
@classmethod
|
||||
def can_migrate(cls, top_level_bpmn_process_diff: WorkflowDiff, subprocesses_diffs: dict[UUID, WorkflowDiff]) -> bool:
|
||||
def safe(result: WorkflowDiff) -> bool:
|
||||
mask = TaskState.COMPLETED | TaskState.STARTED
|
||||
tasks = result.changed + result.removed
|
||||
return len(filter_tasks(tasks, state=mask)) == 0
|
||||
|
||||
for diff in subprocesses_diffs.values():
|
||||
if diff is None or not safe(diff):
|
||||
return False
|
||||
return safe(top_level_bpmn_process_diff)
|
||||
|
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:process id="Process_migration_test_wlm607w" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_top">
|
||||
<bpmn:outgoing>Flow_17db3yp</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_17db3yp" sourceRef="StartEvent_top" targetRef="subprocess_one" />
|
||||
<bpmn:endEvent id="EndEvent_1">
|
||||
<bpmn:incoming>Flow_0m0he21</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0m0he21" sourceRef="subprocess_one" targetRef="EndEvent_1" />
|
||||
<bpmn:subProcess id="subprocess_one">
|
||||
<bpmn:incoming>Flow_17db3yp</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0m0he21</bpmn:outgoing>
|
||||
<bpmn:startEvent id="StartEvent_sub">
|
||||
<bpmn:outgoing>Flow_01eckoj</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_01eckoj" sourceRef="StartEvent_sub" targetRef="manual_task_one" />
|
||||
<bpmn:endEvent id="Event_0fx2psf">
|
||||
<bpmn:incoming>Flow_0s7769x</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0s7769x" sourceRef="manual_task_one" targetRef="Event_0fx2psf" />
|
||||
<bpmn:manualTask id="manual_task_one">
|
||||
<bpmn:incoming>Flow_01eckoj</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0s7769x</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
</bpmn:subProcess>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_migration_test_wlm607w">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_top">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_14za570_di" bpmnElement="EndEvent_1">
|
||||
<dc:Bounds x="462" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_115q6jv_di" bpmnElement="subprocess_one">
|
||||
<dc:Bounds x="280" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_17db3yp_di" bpmnElement="Flow_17db3yp">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="280" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0m0he21_di" bpmnElement="Flow_0m0he21">
|
||||
<di:waypoint x="380" y="177" />
|
||||
<di:waypoint x="462" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_0fjhef4">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1rqwee3" bpmnElement="subprocess_one">
|
||||
<bpmndi:BPMNShape id="Event_0bneyqp_di" bpmnElement="StartEvent_sub">
|
||||
<dc:Bounds x="452" y="302" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_0fx2psf_di" bpmnElement="Event_0fx2psf">
|
||||
<dc:Bounds x="692" y="302" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1lv4tyw_di" bpmnElement="manual_task_one">
|
||||
<dc:Bounds x="540" y="280" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_01eckoj_di" bpmnElement="Flow_01eckoj">
|
||||
<di:waypoint x="488" y="320" />
|
||||
<di:waypoint x="540" y="320" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0s7769x_di" bpmnElement="Flow_0s7769x">
|
||||
<di:waypoint x="640" y="320" />
|
||||
<di:waypoint x="692" y="320" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:process id="Process_migration_test_wlm607w" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_top">
|
||||
<bpmn:outgoing>Flow_17db3yp</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_17db3yp" sourceRef="StartEvent_top" targetRef="subprocess_one" />
|
||||
<bpmn:endEvent id="EndEvent_1">
|
||||
<bpmn:incoming>Flow_0m0he21</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0m0he21" sourceRef="subprocess_one" targetRef="EndEvent_1" />
|
||||
<bpmn:subProcess id="subprocess_one">
|
||||
<bpmn:incoming>Flow_17db3yp</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0m0he21</bpmn:outgoing>
|
||||
<bpmn:startEvent id="StartEvent_sub">
|
||||
<bpmn:outgoing>Flow_01eckoj</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_01eckoj" sourceRef="StartEvent_sub" targetRef="manual_task_one" />
|
||||
<bpmn:endEvent id="Event_0fx2psf">
|
||||
<bpmn:incoming>Flow_17fvyk2</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0s7769x" sourceRef="manual_task_one" targetRef="manual_task_two" />
|
||||
<bpmn:manualTask id="manual_task_one">
|
||||
<bpmn:incoming>Flow_01eckoj</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0s7769x</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="Flow_17fvyk2" sourceRef="manual_task_two" targetRef="Event_0fx2psf" />
|
||||
<bpmn:manualTask id="manual_task_two">
|
||||
<bpmn:incoming>Flow_0s7769x</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_17fvyk2</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
</bpmn:subProcess>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_migration_test_wlm607w">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_top">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_14za570_di" bpmnElement="EndEvent_1">
|
||||
<dc:Bounds x="462" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_115q6jv_di" bpmnElement="subprocess_one">
|
||||
<dc:Bounds x="280" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_17db3yp_di" bpmnElement="Flow_17db3yp">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="280" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0m0he21_di" bpmnElement="Flow_0m0he21">
|
||||
<di:waypoint x="380" y="177" />
|
||||
<di:waypoint x="462" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_0fjhef4">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1rqwee3" bpmnElement="subprocess_one">
|
||||
<bpmndi:BPMNShape id="Event_0bneyqp_di" bpmnElement="StartEvent_sub">
|
||||
<dc:Bounds x="452" y="302" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1lv4tyw_di" bpmnElement="manual_task_one">
|
||||
<dc:Bounds x="540" y="280" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_0fx2psf_di" bpmnElement="Event_0fx2psf">
|
||||
<dc:Bounds x="812" y="302" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0938qt9_di" bpmnElement="manual_task_two">
|
||||
<dc:Bounds x="670" y="280" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_01eckoj_di" bpmnElement="Flow_01eckoj">
|
||||
<di:waypoint x="488" y="320" />
|
||||
<di:waypoint x="540" y="320" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0s7769x_di" bpmnElement="Flow_0s7769x">
|
||||
<di:waypoint x="640" y="320" />
|
||||
<di:waypoint x="670" y="320" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_17fvyk2_di" bpmnElement="Flow_17fvyk2">
|
||||
<di:waypoint x="770" y="320" />
|
||||
<di:waypoint x="812" y="320" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
@ -100,6 +100,38 @@ class BaseTest:
|
||||
|
||||
return process_model
|
||||
|
||||
def create_and_run_process_instance(
|
||||
self,
|
||||
client: FlaskClient,
|
||||
user: UserModel,
|
||||
process_group_id: str | None = "test_group",
|
||||
process_model_id: str | None = "random_fact",
|
||||
bpmn_file_name: str | None = None,
|
||||
bpmn_file_location: str | None = None,
|
||||
) -> tuple[ProcessModelInfo, int]:
|
||||
process_model = self.create_group_and_model_with_bpmn(
|
||||
client=client,
|
||||
user=user,
|
||||
process_group_id=process_group_id,
|
||||
process_model_id=process_model_id,
|
||||
bpmn_file_name=bpmn_file_name,
|
||||
bpmn_file_location=bpmn_file_location,
|
||||
)
|
||||
|
||||
headers = self.logged_in_headers(user)
|
||||
response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
|
||||
assert response.json is not None
|
||||
process_instance_id = response.json["id"]
|
||||
response = client.post(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
|
||||
headers=self.logged_in_headers(user),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json is not None
|
||||
|
||||
return (process_model, int(process_instance_id))
|
||||
|
||||
def create_process_group(
|
||||
self,
|
||||
process_group_id: str,
|
||||
|
@ -1,6 +1,12 @@
|
||||
import os
|
||||
|
||||
from flask.app import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
@ -49,3 +55,110 @@ class TestProcessInstancesController(BaseTest):
|
||||
assert "process_instance" in response.json
|
||||
assert response.json["process_instance"]["id"] == process_instance.id
|
||||
assert response.json["uri_type"] is None
|
||||
|
||||
def test_process_instance_migrate(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
process_model, process_instance_id = self.create_and_run_process_instance(
|
||||
client=client,
|
||||
user=with_super_admin_user,
|
||||
process_model_id="migration-test-with-subprocess",
|
||||
bpmn_file_name="migration-initial.bpmn",
|
||||
)
|
||||
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance_id).first()
|
||||
assert process_instance is not None
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
assert "manual_task_two" not in processor.bpmn_process_instance.spec.task_specs
|
||||
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_one"
|
||||
|
||||
new_file_path = os.path.join(
|
||||
app.instance_path,
|
||||
"..",
|
||||
"..",
|
||||
"tests",
|
||||
"data",
|
||||
"migration-test-with-subprocess",
|
||||
"migration-new.bpmn",
|
||||
)
|
||||
with open(new_file_path) as f:
|
||||
new_contents = f.read().encode()
|
||||
|
||||
SpecFileService.update_file(
|
||||
process_model_info=process_model,
|
||||
file_name="migration-initial.bpmn",
|
||||
binary_data=new_contents,
|
||||
update_process_cache_only=True,
|
||||
)
|
||||
|
||||
processor.suspend()
|
||||
response = client.post(
|
||||
f"/v1.0/process-instance-migrate/{self.modify_process_identifier_for_path_param(process_instance.process_model_identifier)}/{process_instance_id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json is not None
|
||||
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_one"
|
||||
self.complete_next_manual_task(processor)
|
||||
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_two"
|
||||
self.complete_next_manual_task(processor)
|
||||
|
||||
assert process_instance.status == ProcessInstanceStatus.complete.value
|
||||
|
||||
def test_process_instance_check_can_migrate(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
process_model, process_instance_id = self.create_and_run_process_instance(
|
||||
client=client,
|
||||
user=with_super_admin_user,
|
||||
process_model_id="migration-test-with-subprocess",
|
||||
bpmn_file_name="migration-initial.bpmn",
|
||||
)
|
||||
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance_id).first()
|
||||
assert process_instance is not None
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
assert "manual_task_two" not in processor.bpmn_process_instance.spec.task_specs
|
||||
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_one"
|
||||
|
||||
new_file_path = os.path.join(
|
||||
app.instance_path,
|
||||
"..",
|
||||
"..",
|
||||
"tests",
|
||||
"data",
|
||||
"migration-test-with-subprocess",
|
||||
"migration-new.bpmn",
|
||||
)
|
||||
with open(new_file_path) as f:
|
||||
new_contents = f.read().encode()
|
||||
|
||||
SpecFileService.update_file(
|
||||
process_model_info=process_model,
|
||||
file_name="migration-initial.bpmn",
|
||||
binary_data=new_contents,
|
||||
update_process_cache_only=True,
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_instance.process_model_identifier)}/{process_instance_id}/check-can-migrate",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json is not None
|
||||
assert response.json == {"can_migrate": True, "process_instance_id": process_instance.id}
|
||||
|
@ -108,6 +108,10 @@ class TestAuthorizationService(BaseTest):
|
||||
("/process-groups/some-process-group:some-process-model:*", "delete"),
|
||||
("/process-groups/some-process-group:some-process-model:*", "read"),
|
||||
("/process-groups/some-process-group:some-process-model:*", "update"),
|
||||
(
|
||||
"/process-instance-migrate/some-process-group:some-process-model:*",
|
||||
"create",
|
||||
),
|
||||
(
|
||||
"/process-instance-suspend/some-process-group:some-process-model:*",
|
||||
"create",
|
||||
@ -192,6 +196,10 @@ class TestAuthorizationService(BaseTest):
|
||||
("/logs/typeahead-filter-values/some-process-group:some-process-model/*", "read"),
|
||||
("/message-models/some-process-group:some-process-model/*", "read"),
|
||||
("/process-data/some-process-group:some-process-model/*", "read"),
|
||||
(
|
||||
"/process-instance-migrate/some-process-group:some-process-model/*",
|
||||
"create",
|
||||
),
|
||||
(
|
||||
"/process-instance-suspend/some-process-group:some-process-model/*",
|
||||
"create",
|
||||
@ -530,6 +538,7 @@ class TestAuthorizationService(BaseTest):
|
||||
("/messages/*", "create"),
|
||||
("/process-data-file-download/*", "read"),
|
||||
("/process-data/*", "read"),
|
||||
("/process-instance-migrate/*", "create"),
|
||||
("/process-instance-reset/*", "create"),
|
||||
("/process-instance-resume/*", "create"),
|
||||
("/process-instance-suspend/*", "create"),
|
||||
|
@ -1077,6 +1077,53 @@ class TestProcessInstanceProcessor(BaseTest):
|
||||
# mypy thinks this is unreachable but it is reachable. summary can be str | None
|
||||
assert len(process_instance.summary) == 255 # type: ignore
|
||||
|
||||
def test_it_can_update_guids_in_bpmn_process_dict(
|
||||
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_subprocess",
|
||||
process_model_source_directory="loopback_to_subprocess",
|
||||
)
|
||||
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, execution_strategy_name="greedy")
|
||||
|
||||
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.get_task_by_guid(human_task_one.task_id)
|
||||
ProcessInstanceService.complete_form_task(processor, spiff_task, {}, initiator_user, human_task_one)
|
||||
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
|
||||
assert len(process_instance.active_human_tasks) == 1
|
||||
assert len(process_instance.human_tasks) == 2
|
||||
human_task_two = process_instance.active_human_tasks[0]
|
||||
spiff_task = processor.get_task_by_guid(human_task_two.task_id)
|
||||
ProcessInstanceService.complete_form_task(processor, spiff_task, {}, initiator_user, human_task_two)
|
||||
old_tasks = processor.bpmn_process_instance.get_tasks()
|
||||
old_task_names = [t.task_spec.name for t in old_tasks]
|
||||
|
||||
bpmn_process_dict = processor.serialize()
|
||||
task_one_guid = sorted(bpmn_process_dict["tasks"].keys())[0]
|
||||
subprocess_one_guid = sorted(bpmn_process_dict["subprocesses"].keys())[0]
|
||||
ProcessInstanceProcessor.update_guids_on_tasks(bpmn_process_dict)
|
||||
task_two_guid = sorted(bpmn_process_dict["tasks"].keys())[0]
|
||||
subprocess_two_guid = sorted(bpmn_process_dict["subprocesses"].keys())[0]
|
||||
|
||||
assert task_one_guid != task_two_guid
|
||||
assert subprocess_one_guid != subprocess_two_guid
|
||||
|
||||
new_bpmn_process_instance = ProcessInstanceProcessor.initialize_bpmn_process_instance(bpmn_process_dict)
|
||||
new_tasks = new_bpmn_process_instance.get_tasks()
|
||||
new_task_names = [t.task_spec.name for t in new_tasks]
|
||||
assert old_task_names == new_task_names
|
||||
|
||||
# # To test processing times with multiinstance subprocesses
|
||||
# def test_large_multiinstance(
|
||||
# self,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from typing import Any
|
||||
@ -7,9 +8,15 @@ from typing import Any
|
||||
import pytest
|
||||
from flask.app import Flask
|
||||
from SpiffWorkflow.bpmn.util import PendingBpmnEvent # type: ignore
|
||||
from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationNotSafeError
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
|
||||
def _file_content(i: int) -> bytes:
|
||||
@ -146,3 +153,141 @@ class TestProcessInstanceService(BaseTest):
|
||||
datetime.fromisoformat("2023-04-27T20:15:10.626656+00:00"),
|
||||
)
|
||||
)
|
||||
|
||||
def test_it_can_migrate_a_process_instance(
|
||||
self,
|
||||
app: Flask,
|
||||
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/migration-test-with-subprocess",
|
||||
process_model_source_directory="migration-test-with-subprocess",
|
||||
bpmn_file_name="migration-initial.bpmn",
|
||||
)
|
||||
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, execution_strategy_name="greedy")
|
||||
|
||||
initial_tasks = processor.bpmn_process_instance.get_tasks()
|
||||
assert "manual_task_two" not in processor.bpmn_process_instance.spec.task_specs
|
||||
|
||||
new_file_path = os.path.join(
|
||||
app.instance_path,
|
||||
"..",
|
||||
"..",
|
||||
"tests",
|
||||
"data",
|
||||
"migration-test-with-subprocess",
|
||||
"migration-new.bpmn",
|
||||
)
|
||||
with open(new_file_path) as f:
|
||||
new_contents = f.read().encode()
|
||||
|
||||
SpecFileService.update_file(
|
||||
process_model_info=process_model,
|
||||
file_name="migration-initial.bpmn",
|
||||
binary_data=new_contents,
|
||||
update_process_cache_only=True,
|
||||
)
|
||||
|
||||
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
|
||||
ProcessInstanceService.migrate_process_instance_to_newest_model_version(process_instance, user=initiator_user)
|
||||
|
||||
for initial_task in initial_tasks:
|
||||
new_task = processor.bpmn_process_instance.get_task_from_id(initial_task.id)
|
||||
assert new_task is not None
|
||||
assert new_task.last_state_change == initial_task.last_state_change
|
||||
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
|
||||
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_one"
|
||||
self.complete_next_manual_task(processor)
|
||||
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_two"
|
||||
self.complete_next_manual_task(processor)
|
||||
|
||||
assert process_instance.status == ProcessInstanceStatus.complete.value
|
||||
|
||||
def test_it_can_check_if_a_process_instance_can_be_migrated(
|
||||
self,
|
||||
app: Flask,
|
||||
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/migration-test-with-subprocess",
|
||||
process_model_source_directory="migration-test-with-subprocess",
|
||||
bpmn_file_name="migration-initial.bpmn",
|
||||
)
|
||||
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, execution_strategy_name="greedy")
|
||||
|
||||
new_file_path = os.path.join(
|
||||
app.instance_path,
|
||||
"..",
|
||||
"..",
|
||||
"tests",
|
||||
"data",
|
||||
"migration-test-with-subprocess",
|
||||
"migration-new.bpmn",
|
||||
)
|
||||
with open(new_file_path) as f:
|
||||
new_contents = f.read().encode()
|
||||
|
||||
SpecFileService.update_file(
|
||||
process_model_info=process_model,
|
||||
file_name="migration-initial.bpmn",
|
||||
binary_data=new_contents,
|
||||
update_process_cache_only=True,
|
||||
)
|
||||
|
||||
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
|
||||
ProcessInstanceService.check_process_instance_can_be_migrated(process_instance)
|
||||
|
||||
def test_it_raises_if_a_process_instance_cannot_be_migrated_to_new_process_model_version(
|
||||
self,
|
||||
app: Flask,
|
||||
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/migration-test-with-subprocess",
|
||||
process_model_source_directory="migration-test-with-subprocess",
|
||||
bpmn_file_name="migration-initial.bpmn",
|
||||
)
|
||||
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, execution_strategy_name="greedy")
|
||||
human_task_one = process_instance.active_human_tasks[0]
|
||||
assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_one"
|
||||
self.complete_next_manual_task(processor)
|
||||
assert process_instance.status == ProcessInstanceStatus.complete.value
|
||||
|
||||
new_file_path = os.path.join(
|
||||
app.instance_path,
|
||||
"..",
|
||||
"..",
|
||||
"tests",
|
||||
"data",
|
||||
"migration-test-with-subprocess",
|
||||
"migration-new.bpmn",
|
||||
)
|
||||
with open(new_file_path) as f:
|
||||
new_contents = f.read().encode()
|
||||
|
||||
SpecFileService.update_file(
|
||||
process_model_info=process_model,
|
||||
file_name="migration-initial.bpmn",
|
||||
binary_data=new_contents,
|
||||
update_process_cache_only=True,
|
||||
)
|
||||
|
||||
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
|
||||
|
||||
with pytest.raises(ProcessInstanceMigrationNotSafeError):
|
||||
ProcessInstanceService.check_process_instance_can_be_migrated(process_instance)
|
||||
|
Loading…
x
Reference in New Issue
Block a user