diff --git a/migrations/versions/d6e5b3af0908_.py b/migrations/versions/d6e5b3af0908_.py new file mode 100644 index 00000000..f47b4b57 --- /dev/null +++ b/migrations/versions/d6e5b3af0908_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: d6e5b3af0908 +Revises: 9f0b1662a8af +Create Date: 2023-02-27 11:10:28.058014 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd6e5b3af0908' +down_revision = '9f0b1662a8af' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('human_task', sa.Column('bpmn_process_identifier', sa.String(length=255), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('human_task', 'bpmn_process_identifier') + # ### end Alembic commands ### diff --git a/src/spiffworkflow_backend/models/human_task.py b/src/spiffworkflow_backend/models/human_task.py index 3317f773..5cc208b1 100644 --- a/src/spiffworkflow_backend/models/human_task.py +++ b/src/spiffworkflow_backend/models/human_task.py @@ -34,6 +34,10 @@ class HumanTaskModel(SpiffworkflowBaseDBModel): lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id)) completed_by_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=True) # type: ignore + completed_by_user = relationship( + "UserModel", foreign_keys=[completed_by_user_id], viewonly=True + ) + actual_owner_id: int = db.Column(ForeignKey(UserModel.id)) # type: ignore # actual_owner: RelationshipProperty[UserModel] = relationship(UserModel) @@ -49,6 +53,7 @@ class HumanTaskModel(SpiffworkflowBaseDBModel): task_type: str = db.Column(db.String(50)) task_status: str = db.Column(db.String(50)) process_model_display_name: str = db.Column(db.String(255)) + bpmn_process_identifier: str = db.Column(db.String(255)) completed: bool = db.Column(db.Boolean, default=False, nullable=False, index=True) human_task_users = relationship("HumanTaskUserModel", cascade="delete") @@ -74,8 +79,8 @@ class HumanTaskModel(SpiffworkflowBaseDBModel): new_task.process_model_display_name = task.process_model_display_name if hasattr(task, "process_group_identifier"): new_task.process_group_identifier = task.process_group_identifier - if hasattr(task, "process_model_identifier"): - new_task.process_model_identifier = task.process_model_identifier + if hasattr(task, "bpmn_process_identifier"): + new_task.bpmn_process_identifier = task.bpmn_process_identifier # human tasks only have status when getting the list on the home page # and it comes from the process_instance. it should not be confused with task_status. diff --git a/src/spiffworkflow_backend/models/task.py b/src/spiffworkflow_backend/models/task.py index 4012b077..148df231 100644 --- a/src/spiffworkflow_backend/models/task.py +++ b/src/spiffworkflow_backend/models/task.py @@ -45,6 +45,7 @@ class Task: process_model_display_name: Union[str, None] = None, process_group_identifier: Union[str, None] = None, process_model_identifier: Union[str, None] = None, + bpmn_process_identifier: Union[str, None] = None, form_schema: Union[dict, None] = None, form_ui_schema: Union[dict, None] = None, parent: Optional[str] = None, @@ -76,6 +77,7 @@ class Task: self.process_instance_status = process_instance_status self.process_group_identifier = process_group_identifier self.process_model_identifier = process_model_identifier + self.bpmn_process_identifier = bpmn_process_identifier self.process_model_display_name = process_model_display_name self.form_schema = form_schema self.form_ui_schema = form_ui_schema @@ -122,6 +124,7 @@ class Task: "process_model_display_name": self.process_model_display_name, "process_group_identifier": self.process_group_identifier, "process_model_identifier": self.process_model_identifier, + "bpmn_process_identifier": self.bpmn_process_identifier, "form_schema": self.form_schema, "form_ui_schema": self.form_ui_schema, "parent": self.parent, diff --git a/src/spiffworkflow_backend/models/user.py b/src/spiffworkflow_backend/models/user.py index 464bdc8b..f32a35d7 100644 --- a/src/spiffworkflow_backend/models/user.py +++ b/src/spiffworkflow_backend/models/user.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any import jwt import marshmallow @@ -82,6 +83,13 @@ class UserModel(SpiffworkflowBaseDBModel): # # return instance + def as_dict(self) -> dict[str, Any]: + # dump the user using our json encoder and then load it back up as a dict + # to remove unwanted field types + user_as_json_string = current_app.json.dumps(self) + user_dict: dict[str, Any] = current_app.json.loads(user_as_json_string) + return user_dict + class UserModelSchema(Schema): """UserModelSchema.""" diff --git a/src/spiffworkflow_backend/scripts/get_last_user_completing_task.py b/src/spiffworkflow_backend/scripts/get_last_user_completing_task.py new file mode 100644 index 00000000..8d63610b --- /dev/null +++ b/src/spiffworkflow_backend/scripts/get_last_user_completing_task.py @@ -0,0 +1,48 @@ +"""Get current user.""" +from typing import Any + +from spiffworkflow_backend.models.human_task import HumanTaskModel +from spiffworkflow_backend.models.script_attributes_context import ( + ScriptAttributesContext, +) +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.scripts.script import Script + + +class GetLastUserCompletingTask(Script): + @staticmethod + def requires_privileged_permissions() -> bool: + """We have deemed this function safe to run without elevated permissions.""" + return False + + def get_description(self) -> str: + return """Return the last user who completed the given task.""" + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *_args: Any, + **kwargs: Any, + ) -> Any: + """Run.""" + # dump the user using our json encoder and then load it back up as a dict + # to remove unwanted field types + if len(_args) == 2: + bpmn_process_identifier = _args[0] + task_name = _args[1] + else: + bpmn_process_identifier = kwargs["bpmn_process_identifier"] + task_name = kwargs["task_bpmn_identifier"] + + human_task = ( + HumanTaskModel.query.filter_by( + process_instance_id=script_attributes_context.process_instance_id, + bpmn_process_identifier=bpmn_process_identifier, + task_name=task_name, + ) + .order_by(HumanTaskModel.id.desc()) # type: ignore + .join(UserModel, UserModel.id == HumanTaskModel.completed_by_user_id) + .first() + ) + + return human_task.completed_by_user.as_dict() diff --git a/src/spiffworkflow_backend/scripts/get_process_initiator_user.py b/src/spiffworkflow_backend/scripts/get_process_initiator_user.py new file mode 100644 index 00000000..266fa57b --- /dev/null +++ b/src/spiffworkflow_backend/scripts/get_process_initiator_user.py @@ -0,0 +1,36 @@ +"""Get current user.""" +from typing import Any + +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.models.script_attributes_context import ( + ScriptAttributesContext, +) +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.scripts.script import Script + + +class GetProcessInitiatorUser(Script): + @staticmethod + def requires_privileged_permissions() -> bool: + """We have deemed this function safe to run without elevated permissions.""" + return False + + def get_description(self) -> str: + return """Return the user that initiated the process instance.""" + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *_args: Any, + **kwargs: Any, + ) -> Any: + """Run.""" + process_instance = ( + ProcessInstanceModel.query.filter_by( + id=script_attributes_context.process_instance_id + ) + .join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) + .first() + ) + + return process_instance.process_initiator.as_dict() diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index a2bac404..8c76370d 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -869,6 +869,7 @@ class ProcessInstanceProcessor: process_instance_id=self.process_instance_model.id, completed=False ).all() ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks() + process_model_display_name = "" process_model_info = self.process_model_service.get_process_model( self.process_instance_model.process_model_identifier @@ -887,6 +888,10 @@ class ProcessInstanceProcessor: ) extensions = task_spec.extensions + # in the xml, it's the id attribute. this identifies the process where the activity lives. + # if it's in a subprocess, it's the inner process. + bpmn_process_identifier = ready_or_waiting_task.workflow.name + form_file_name = None ui_form_file_name = None if "properties" in extensions: @@ -906,6 +911,7 @@ class ProcessInstanceProcessor: human_task = HumanTaskModel( process_instance_id=self.process_instance_model.id, process_model_display_name=process_model_display_name, + bpmn_process_identifier=bpmn_process_identifier, form_file_name=form_file_name, ui_form_file_name=ui_form_file_name, task_id=str(ready_or_waiting_task.id), @@ -1635,7 +1641,6 @@ class ProcessInstanceProcessor: details_model.end_in_seconds = time.time() details_model.task_json = self.get_task_json_from_spiff_task(task) db.session.add(details_model) - # this is the thing that actually commits the db transaction (on behalf of the other updates above as well) self.save() diff --git a/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn b/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn index d4f1aa5d..9b15cb09 100644 --- a/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn +++ b/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn @@ -1,6 +1,6 @@ - + Flow_1my9ag5 @@ -28,7 +28,7 @@ form_ui_hidden_fields = ["veryImportantFieldButOnlySometimes", "building.floor"] - + diff --git a/tests/data/error/instructions_error.bpmn b/tests/data/error/instructions_error.bpmn index 24039bbb..1db55f39 100644 --- a/tests/data/error/instructions_error.bpmn +++ b/tests/data/error/instructions_error.bpmn @@ -1,6 +1,6 @@ - + Flow_0smvjir @@ -21,7 +21,7 @@ Department: {{ department }} - + diff --git a/tests/data/get_localtime/get_localtime.bpmn b/tests/data/get_localtime/get_localtime.bpmn index 5660ba0b..2efa2fa6 100644 --- a/tests/data/get_localtime/get_localtime.bpmn +++ b/tests/data/get_localtime/get_localtime.bpmn @@ -1,6 +1,6 @@ - + Flow_0ijucqh @@ -40,7 +40,7 @@ localtime = get_localtime(some_time, timezone) - + diff --git a/tests/data/manual_task/manual_task.bpmn b/tests/data/manual_task/manual_task.bpmn index aefbb376..4f0fba72 100644 --- a/tests/data/manual_task/manual_task.bpmn +++ b/tests/data/manual_task/manual_task.bpmn @@ -1,6 +1,6 @@ - + Flow_1xlck7g @@ -18,7 +18,7 @@ - + diff --git a/tests/data/model_with_lanes/lanes.bpmn b/tests/data/model_with_lanes/lanes.bpmn index 3ee43501..b396bf71 100644 --- a/tests/data/model_with_lanes/lanes.bpmn +++ b/tests/data/model_with_lanes/lanes.bpmn @@ -1,13 +1,13 @@ - + - + StartEvent_1 - initator_one + initiator_one Event_06f4e68 initiator_two @@ -18,18 +18,18 @@ Flow_1tbyols - - - + + + - This is initiator user? + This is for the initiator user Flow_1tbyols Flow_16ppta1 - This is finance user? + This is for a Finance Team user Flow_16ppta1 Flow_1cfcauf @@ -41,7 +41,8 @@ - This is initiator again? + This is initiator again + Flow_1cfcauf Flow_0x92f7d @@ -63,7 +64,7 @@ - + diff --git a/tests/data/model_with_lanes/lanes_with_owner_dict.bpmn b/tests/data/model_with_lanes/lanes_with_owner_dict.bpmn index 0c2af8d4..9d0f2a30 100644 --- a/tests/data/model_with_lanes/lanes_with_owner_dict.bpmn +++ b/tests/data/model_with_lanes/lanes_with_owner_dict.bpmn @@ -1,9 +1,9 @@ - + - + StartEvent_1 diff --git a/tests/data/simple_form/simple_form.bpmn b/tests/data/simple_form/simple_form.bpmn index 41056173..a2f29fd3 100644 --- a/tests/data/simple_form/simple_form.bpmn +++ b/tests/data/simple_form/simple_form.bpmn @@ -1,6 +1,6 @@ - + Flow_0smvjir @@ -14,6 +14,7 @@ Hello {{ name }} Department: {{ department }} + user_completing_task = get_last_user_completing_task("Process_WithForm", "Activity_SimpleForm") Flow_1ly1khd Flow_1boyhcj @@ -25,13 +26,14 @@ Department: {{ department }} + process_initiator_user = get_process_initiator_user() Flow_0smvjir Flow_1ly1khd - + diff --git a/tests/data/simple_form_with_error/simple_form_with_error.bpmn b/tests/data/simple_form_with_error/simple_form_with_error.bpmn index 351d53a6..43d3d116 100644 --- a/tests/data/simple_form_with_error/simple_form_with_error.bpmn +++ b/tests/data/simple_form_with_error/simple_form_with_error.bpmn @@ -1,6 +1,6 @@ - + Flow_0smvjir @@ -31,7 +31,7 @@ Department: {{ department }} - + diff --git a/tests/data/simple_script/simple_script.bpmn b/tests/data/simple_script/simple_script.bpmn index 6e14807f..f5efba61 100644 --- a/tests/data/simple_script/simple_script.bpmn +++ b/tests/data/simple_script/simple_script.bpmn @@ -1,6 +1,6 @@ - + Flow_0r3ua0i @@ -48,7 +48,7 @@ b = 2 - + diff --git a/tests/spiffworkflow_backend/integration/test_process_api.py b/tests/spiffworkflow_backend/integration/test_process_api.py index 93a2da88..881f11ca 100644 --- a/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/tests/spiffworkflow_backend/integration/test_process_api.py @@ -583,7 +583,7 @@ class TestProcessApi(BaseTest): # We should get 5 back, as one of the items in the cache is a decision. assert len(response.json) == 5 simple_form = next( - p for p in response.json if p["identifier"] == "Proccess_WithForm" + p for p in response.json if p["identifier"] == "Process_WithForm" ) assert simple_form["display_name"] == "Process With Form" assert simple_form["process_model_id"] == "test_group_one/simple_form" diff --git a/tests/spiffworkflow_backend/scripts/test_get_last_user_completing_task.py b/tests/spiffworkflow_backend/scripts/test_get_last_user_completing_task.py new file mode 100644 index 00000000..d6533eae --- /dev/null +++ b/tests/spiffworkflow_backend/scripts/test_get_last_user_completing_task.py @@ -0,0 +1,69 @@ +"""Test_get_localtime.""" +from flask.app import Flask +from flask.testing import FlaskClient +from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec + +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.authorization_service import AuthorizationService +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) +from spiffworkflow_backend.services.process_instance_service import ( + ProcessInstanceService, +) + + +class TestGetLastUserCompletingTask(BaseTest): + def test_get_last_user_completing_task_script_works( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_sets_permission_correctly_on_human_task.""" + self.create_process_group( + client, with_super_admin_user, "test_group", "test_group" + ) + initiator_user = self.find_or_create_user("initiator_user") + assert initiator_user.principal is not None + AuthorizationService.import_permissions_from_yaml_file() + + process_model = load_test_spec( + process_model_id="misc/category_number_one/simple_form", + # bpmn_file_name="simp.bpmn", + process_model_source_directory="simple_form", + ) + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=initiator_user + ) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + + assert len(process_instance.active_human_tasks) == 1 + human_task = process_instance.active_human_tasks[0] + assert len(human_task.potential_owners) == 1 + assert human_task.potential_owners[0] == initiator_user + + spiff_task = processor.__class__.get_task_by_bpmn_identifier( + human_task.task_name, processor.bpmn_process_instance + ) + ProcessInstanceService.complete_form_task( + processor, spiff_task, {"name": "HEY"}, initiator_user, human_task + ) + + assert len(process_instance.active_human_tasks) == 1 + human_task = process_instance.active_human_tasks[0] + spiff_task = processor.__class__.get_task_by_bpmn_identifier( + human_task.task_name, processor.bpmn_process_instance + ) + ProcessInstanceService.complete_form_task( + processor, spiff_task, {}, initiator_user, human_task + ) + + assert spiff_task is not None + assert ( + initiator_user.username + == spiff_task.get_data("user_completing_task")["username"] + ) diff --git a/tests/spiffworkflow_backend/scripts/test_get_process_initiator_user.py b/tests/spiffworkflow_backend/scripts/test_get_process_initiator_user.py new file mode 100644 index 00000000..5e734227 --- /dev/null +++ b/tests/spiffworkflow_backend/scripts/test_get_process_initiator_user.py @@ -0,0 +1,62 @@ +"""Test_get_localtime.""" +from spiffworkflow_backend.services.authorization_service import AuthorizationService +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec + +from flask.app import Flask +from flask.testing import FlaskClient +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) +from spiffworkflow_backend.services.process_instance_service import ( + ProcessInstanceService, +) + + +class TestGetProcessInitiatorUser(BaseTest): + + def test_get_process_initiator_user( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_sets_permission_correctly_on_human_task.""" + self.create_process_group( + client, with_super_admin_user, "test_group", "test_group" + ) + initiator_user = self.find_or_create_user("initiator_user") + assert initiator_user.principal is not None + AuthorizationService.import_permissions_from_yaml_file() + + process_model = load_test_spec( + process_model_id="misc/category_number_one/simple_form", + # bpmn_file_name="simp.bpmn", + process_model_source_directory="simple_form", + ) + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=initiator_user + ) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + + assert len(process_instance.active_human_tasks) == 1 + human_task = process_instance.active_human_tasks[0] + assert len(human_task.potential_owners) == 1 + assert human_task.potential_owners[0] == initiator_user + + spiff_task = processor.__class__.get_task_by_bpmn_identifier( + human_task.task_name, processor.bpmn_process_instance + ) + ProcessInstanceService.complete_form_task( + processor, spiff_task, {"name": "HEY"}, initiator_user, human_task + ) + + assert spiff_task is not None + assert ( + initiator_user.username + == spiff_task.get_data("process_initiator_user")["username"] + )