diff --git a/spiffworkflow-backend/migrations/versions/567d22ded3af_.py b/spiffworkflow-backend/migrations/versions/567d22ded3af_.py new file mode 100644 index 000000000..1e41244d2 --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/567d22ded3af_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 567d22ded3af +Revises: 2fe2830f45e1 +Create Date: 2023-03-03 08:38:25.855923 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '567d22ded3af' +down_revision = '2fe2830f45e1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('bpmn_process_definition_relationship_unique', 'bpmn_process_definition_relationship', ['bpmn_process_definition_parent_id', 'bpmn_process_definition_child_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('bpmn_process_definition_relationship_unique', 'bpmn_process_definition_relationship', type_='unique') + # ### end Alembic commands ### diff --git a/spiffworkflow-backend/migrations/versions/6315ff2525b0_.py b/spiffworkflow-backend/migrations/versions/6315ff2525b0_.py new file mode 100644 index 000000000..d21097a2c --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/6315ff2525b0_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 6315ff2525b0 +Revises: 567d22ded3af +Create Date: 2023-03-03 09:19:28.098057 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6315ff2525b0' +down_revision = '567d22ded3af' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('process_instance', sa.Column('bpmn_process_definition_id', sa.Integer(), nullable=False)) + op.create_foreign_key(None, 'process_instance', 'bpmn_process_definition', ['bpmn_process_definition_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'process_instance', type_='foreignkey') + op.drop_column('process_instance', 'bpmn_process_definition_id') + # ### end Alembic commands ### diff --git a/spiffworkflow-backend/migrations/versions/def2cbb0ca6b_.py b/spiffworkflow-backend/migrations/versions/def2cbb0ca6b_.py new file mode 100644 index 000000000..33bb26948 --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/def2cbb0ca6b_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: def2cbb0ca6b +Revises: 6315ff2525b0 +Create Date: 2023-03-03 09:23:19.480250 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'def2cbb0ca6b' +down_revision = '6315ff2525b0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('process_instance', 'bpmn_process_definition_id', + existing_type=mysql.INTEGER(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('process_instance', 'bpmn_process_definition_id', + existing_type=mysql.INTEGER(), + nullable=False) + # ### end Alembic commands ### 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 348aae5d1..d60a6c0dd 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition.py @@ -21,6 +21,7 @@ class BpmnProcessDefinitionModel(SpiffworkflowBaseDBModel): properties_json: str = db.Column(db.JSON, nullable=False) # process or subprocess + # FIXME: will probably ignore for now since we do not strictly need it type: str = db.Column(db.String(32), nullable=False, index=True) # TODO: remove these from process_instance diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition_relationship.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition_relationship.py index 80f5c9bb2..218d226a7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition_relationship.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/bpmn_process_definition_relationship.py @@ -1,4 +1,5 @@ from __future__ import annotations +from sqlalchemy import UniqueConstraint from sqlalchemy.orm import deferred from sqlalchemy import ForeignKey from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel @@ -9,6 +10,12 @@ from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel class BpmnProcessDefinitionRelationshipModel(SpiffworkflowBaseDBModel): __tablename__ = "bpmn_process_definition_relationship" + __table_args__ = ( + UniqueConstraint( + "bpmn_process_definition_parent_id", "bpmn_process_definition_child_id", name="bpmn_process_definition_relationship_unique" + ), + ) + id: int = db.Column(db.Integer, primary_key=True) bpmn_process_definition_parent_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=False) # type: ignore bpmn_process_definition_child_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=False) # type: ignore diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index f12cbec7c..4cfb52b7b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -1,5 +1,6 @@ """Process_instance.""" from __future__ import annotations +from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from typing import Any from typing import cast @@ -74,6 +75,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): ) process_instance_data = relationship("ProcessInstanceDataModel", cascade="delete") + bpmn_process_definition_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=True) # type: ignore + bpmn_process_definition = relationship(BpmnProcessDefinitionModel) + active_human_tasks = relationship( "HumanTaskModel", primaryjoin=( 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 172b9542b..aaa8a9c35 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -1,5 +1,7 @@ """Process_instance_processor.""" -import _strptime # type: ignore +import _strptime +from spiffworkflow_backend.models import serialized_bpmn_definition # type: ignore +from spiffworkflow_backend.models.bpmn_process_definition_relationship import BpmnProcessDefinitionRelationshipModel # noqa: F401 import decimal import json import logging @@ -55,6 +57,7 @@ from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore from sqlalchemy import text from spiffworkflow_backend.exceptions.api_error import ApiError +from spiffworkflow_backend.models import bpmn_process_definition_relationship from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.file import File @@ -526,9 +529,32 @@ class ProcessInstanceProcessor: def _get_full_bpmn_json(cls, process_instance_model: ProcessInstanceModel) -> dict: if process_instance_model.serialized_bpmn_definition_id is None: return {} - serialized_bpmn_definition = process_instance_model.serialized_bpmn_definition + # serialized_bpmn_definition = process_instance_model.serialized_bpmn_definition + # print(f"serialized_bpmn_definition.static_json: {serialized_bpmn_definition.static_json}") + # loaded_json: dict = json.loads(serialized_bpmn_definition.static_json) # or "{}") + + serialized_bpmn_definition = {} + bpmn_process_definition = BpmnProcessDefinitionModel.query.filter_by(id=process_instance_model.bpmn_process_definition_id).first() + task_definitions = TaskDefinitionModel.query.filter_by(bpmn_process_definition_id=process_instance_model.bpmn_process_definition_id).all() + if bpmn_process_definition is not None: + serialized_bpmn_definition = {"serializer_version": cls.SERIALIZER_VERSION, "spec": {}, "subprocess_specs": {}} + bpmn_process_definition_dict = json.loads(bpmn_process_definition.properties_json) + bpmn_process_definition_dict['task_specs'] = {} + for task_definition in task_definitions: + bpmn_process_definition_dict['task_specs'][task_definition.bpmn_identifier] = json.loads(task_definition.properties_json) + serialized_bpmn_definition['spec'] = bpmn_process_definition_dict + + bpmn_process_subprocess_definitions = BpmnProcessDefinitionRelationshipModel.query.filter_by(bpmn_process_definition_parent_id=bpmn_process_definition.id).all() + for bpmn_process_subprocess_definition in bpmn_process_subprocess_definitions: + subprocess_task_definitions = TaskDefinitionModel.query.filter_by(bpmn_process_definition_id=bpmn_process_subprocess_definition.id).all() + bpmn_process_subprocess_dict = json.loads(bpmn_process_definition.properties_json) + bpmn_process_subprocess_dict['task_specs'] = {} + for subprocess_task_definition in subprocess_task_definitions: + bpmn_process_subprocess_dict['task_specs'][subprocess_task_definition.bpmn_identifier] = json.loads(subprocess_task_definition.properties_json) + serialized_bpmn_definition['subprocess_specs'][bpmn_process_subprocess_definition.bpmn_identifier] = bpmn_process_subprocess_dict + loaded_json: dict = serialized_bpmn_definition + process_instance_data = process_instance_model.process_instance_data - loaded_json: dict = json.loads(serialized_bpmn_definition.static_json or "{}") loaded_json.update(json.loads(process_instance_data.runtime_json)) return loaded_json @@ -909,19 +935,15 @@ class ProcessInstanceProcessor: self.process_instance_model.process_instance_data = process_instance_data - def _store_bpmn_process_definitions(self, process_bpmn_properties: dict) -> None: - # for process_bpmn_identifier, process_bpmn_properties in bpmn_spec_dict.items(): - print(f"process_bpmn_properties: {process_bpmn_properties}") + def _store_bpmn_process_definition(self, process_bpmn_properties: dict, bpmn_process_definition_parent: Optional[BpmnProcessDefinitionModel] = None) -> BpmnProcessDefinitionModel: process_bpmn_identifier = process_bpmn_properties['name'] new_hash_digest = sha256( json.dumps(process_bpmn_properties, sort_keys=True).encode("utf8") ).hexdigest() - bpmn_process_definition = BpmnProcessDefinitionModel.query.filter_by( + bpmn_process_definition: Optional[BpmnProcessDefinitionModel] = BpmnProcessDefinitionModel.query.filter_by( hash=new_hash_digest ).first() if bpmn_process_definition is None: - # print(f"process_bpmn_identifier: {process_bpmn_identifier}") - print(f"process_bpmn_properties: {process_bpmn_properties}") task_specs = process_bpmn_properties.pop("task_specs") bpmn_process_definition = BpmnProcessDefinitionModel( hash=new_hash_digest, bpmn_identifier=process_bpmn_identifier, properties_json=json.dumps(process_bpmn_properties), type="process" @@ -937,6 +959,19 @@ class ProcessInstanceProcessor: ) db.session.add(task_definition) + if bpmn_process_definition_parent: + bpmn_process_definition_relationship = BpmnProcessDefinitionRelationshipModel.query.filter_by( + bpmn_process_definition_parent_id=bpmn_process_definition_parent.id, + bpmn_process_definition_child_id=bpmn_process_definition.id, + ).first() + if bpmn_process_definition_relationship is None: + bpmn_process_definition_relationship = BpmnProcessDefinitionRelationshipModel( + bpmn_process_definition_parent_id=bpmn_process_definition_parent.id, + bpmn_process_definition_child_id=bpmn_process_definition.id, + ) + db.session.add(bpmn_process_definition_relationship) + return bpmn_process_definition + def _add_bpmn_json_records_new(self) -> None: """Adds serialized_bpmn_definition and process_instance_data records to the db session. @@ -952,7 +987,11 @@ class ProcessInstanceProcessor: else: process_instance_data_dict[bpmn_key] = bpmn_dict[bpmn_key] - self._store_bpmn_process_definitions(bpmn_spec_dict['spec']) + bpmn_process_definition_parent = self._store_bpmn_process_definition(bpmn_spec_dict['spec']) + for process_bpmn_properties in bpmn_spec_dict['subprocess_specs'].values(): + self._store_bpmn_process_definition(process_bpmn_properties, bpmn_process_definition_parent) + self.process_instance_model.bpmn_process_definition = bpmn_process_definition_parent + # # FIXME: always save new hash until we get updated Spiff without loopresettask # # if self.process_instance_model.serialized_bpmn_definition_id is None: diff --git a/spiffworkflow-backend/tests/data/dot_notation/diagram.bpmn b/spiffworkflow-backend/tests/data/dot_notation/diagram.bpmn index 96bdce894..5d6829420 100644 --- a/spiffworkflow-backend/tests/data/dot_notation/diagram.bpmn +++ b/spiffworkflow-backend/tests/data/dot_notation/diagram.bpmn @@ -24,14 +24,14 @@ - + Flow_0dbnzbi - - + + Flow_0nt355i - + @@ -53,13 +53,13 @@ - + - + diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 3055d4b2c..f450729ce 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1301,10 +1301,11 @@ class TestProcessApi(BaseTest): assert create_response.json is not None assert create_response.status_code == 201 process_instance_id = create_response.json["id"] - client.post( + run_response = client.post( f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) + assert run_response.status_code == 200 show_response = client.get( f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}?process_identifier={spec_reference.identifier}", headers=self.logged_in_headers(with_super_admin_user), diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py index 59a0fee8d..c646a7540 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py @@ -13,9 +13,7 @@ from spiffworkflow_backend.services.process_instance_service import ( class TestDotNotation(BaseTest): - """TestVariousBpmnConstructs.""" - - def test_dot_notation( + def test_dot_notation_in_message_path( self, app: Flask, client: FlaskClient,