From 96e14817fa735f0fdee3e5ced460377a67f3c5aa Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 11:16:06 -0500 Subject: [PATCH 1/9] some initial changes to refactor report filters w/ burnettk --- spiffworkflow-backend/migrations/env.py | 2 + .../{b86f7cc3a74b_.py => b99a4cb94b5b_.py} | 13 +- .../models/human_task.py | 7 +- .../models/spiff_step_details.py | 12 +- .../routes/process_api_blueprint.py | 203 +++--------------- .../services/process_instance_processor.py | 6 +- .../process_instance_report_service.py | 186 ++++++++++++++++ .../test_process_instance_report_service.py | 34 ++- 8 files changed, 270 insertions(+), 193 deletions(-) rename spiffworkflow-backend/migrations/versions/{b86f7cc3a74b_.py => b99a4cb94b5b_.py} (98%) diff --git a/spiffworkflow-backend/migrations/env.py b/spiffworkflow-backend/migrations/env.py index 630e381a..68feded2 100644 --- a/spiffworkflow-backend/migrations/env.py +++ b/spiffworkflow-backend/migrations/env.py @@ -1,3 +1,5 @@ +from __future__ import with_statement + import logging from logging.config import fileConfig diff --git a/spiffworkflow-backend/migrations/versions/b86f7cc3a74b_.py b/spiffworkflow-backend/migrations/versions/b99a4cb94b5b_.py similarity index 98% rename from spiffworkflow-backend/migrations/versions/b86f7cc3a74b_.py rename to spiffworkflow-backend/migrations/versions/b99a4cb94b5b_.py index 30138654..ec359254 100644 --- a/spiffworkflow-backend/migrations/versions/b86f7cc3a74b_.py +++ b/spiffworkflow-backend/migrations/versions/b99a4cb94b5b_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: b86f7cc3a74b +Revision ID: b99a4cb94b5b Revises: -Create Date: 2022-12-19 16:20:27.715487 +Create Date: 2022-12-20 10:45:08.295317 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'b86f7cc3a74b' +revision = 'b99a4cb94b5b' down_revision = None branch_labels = None depends_on = None @@ -179,8 +179,9 @@ def upgrade(): op.create_table('human_task', sa.Column('id', sa.Integer(), nullable=False), sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('actual_owner_id', sa.Integer(), nullable=True), sa.Column('lane_assignment_id', sa.Integer(), nullable=True), + sa.Column('completed_by_user_id', sa.Integer(), nullable=True), + sa.Column('actual_owner_id', sa.Integer(), nullable=True), sa.Column('form_file_name', sa.String(length=50), nullable=True), sa.Column('ui_form_file_name', sa.String(length=50), nullable=True), sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), @@ -193,6 +194,7 @@ def upgrade(): sa.Column('process_model_display_name', sa.String(length=255), nullable=True), sa.Column('completed', sa.Boolean(), nullable=False), sa.ForeignKeyConstraint(['actual_owner_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['completed_by_user_id'], ['user.id'], ), sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ), sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), sa.PrimaryKeyConstraint('id'), @@ -259,9 +261,6 @@ def upgrade(): sa.Column('spiff_step', sa.Integer(), nullable=False), sa.Column('task_json', sa.JSON(), nullable=False), sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False), - sa.Column('completed_by_user_id', sa.Integer(), nullable=True), - sa.Column('lane_assignment_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ), sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), sa.PrimaryKeyConstraint('id') ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py index 26b99f39..d4b2caf7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py @@ -31,13 +31,16 @@ class HumanTaskModel(SpiffworkflowBaseDBModel): db.UniqueConstraint("task_id", "process_instance_id", name="human_task_unique"), ) - actual_owner: RelationshipProperty[UserModel] = relationship(UserModel) id: int = db.Column(db.Integer, primary_key=True) process_instance_id: int = db.Column( ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore ) - actual_owner_id: int = db.Column(ForeignKey(UserModel.id)) lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id)) + completed_by_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=True) + + actual_owner_id: int = db.Column(ForeignKey(UserModel.id)) + # actual_owner: RelationshipProperty[UserModel] = relationship(UserModel) + form_file_name: str | None = db.Column(db.String(50)) ui_form_file_name: str | None = db.Column(db.String(50)) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py index 9afb5d07..11a5c7a9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py @@ -1,5 +1,6 @@ """Spiff_step_details.""" from dataclasses import dataclass +from spiffworkflow_backend.models.human_task import HumanTaskModel from typing import Optional from flask_bpmn.models.db import db @@ -20,10 +21,13 @@ class SpiffStepDetailsModel(SpiffworkflowBaseDBModel): process_instance_id: int = db.Column( ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore ) + # human_task_id: int = db.Column( + # ForeignKey(HumanTaskModel.id) # type: ignore + # ) spiff_step: int = db.Column(db.Integer, nullable=False) task_json: dict = deferred(db.Column(db.JSON, nullable=False)) # type: ignore timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False) - completed_by_user_id: int = db.Column(db.Integer, nullable=True) - lane_assignment_id: Optional[int] = db.Column( - ForeignKey(GroupModel.id), nullable=True - ) + # completed_by_user_id: int = db.Column(db.Integer, nullable=True) + # lane_assignment_id: Optional[int] = db.Column( + # ForeignKey(GroupModel.id), nullable=True + # ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index f01635b0..9afba606 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -860,7 +860,7 @@ def process_instance_list_for_me( user_filter: Optional[bool] = False, report_identifier: Optional[str] = None, report_id: Optional[int] = None, - group_identifier: Optional[str] = None, + user_group_identifier: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list_for_me.""" return process_instance_list( @@ -875,7 +875,7 @@ def process_instance_list_for_me( user_filter=user_filter, report_identifier=report_identifier, report_id=report_id, - group_identifier=group_identifier, + user_group_identifier=user_group_identifier, with_relation_to_me=True, ) @@ -896,183 +896,46 @@ def process_instance_list( user_filter: Optional[bool] = False, report_identifier: Optional[str] = None, report_id: Optional[int] = None, - group_identifier: Optional[str] = None, + user_group_identifier: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list.""" process_instance_report = ProcessInstanceReportService.report_with_identifier( g.user, report_id, report_identifier ) - print(f"with_relation_to_me: {with_relation_to_me}") if user_filter: report_filter = ProcessInstanceReportFilter( - process_model_identifier, - start_from, - start_to, - end_from, - end_to, - process_status.split(",") if process_status else None, - initiated_by_me, - with_tasks_completed_by_me, - with_tasks_completed_by_my_group, - with_relation_to_me, + process_model_identifier=process_model_identifier, + user_group_identifier=user_group_identifier, + start_from=start_from, + start_to=start_to, + end_from=end_from, + end_to=end_to, + initiated_by_me=initiated_by_me, + with_tasks_completed_by_me=with_tasks_completed_by_me, + with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, + with_relation_to_me=with_relation_to_me, + process_status=process_status.split(",") if process_status else None, ) else: report_filter = ( ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report, - process_model_identifier, - start_from, - start_to, - end_from, - end_to, - process_status, - initiated_by_me, - with_tasks_completed_by_me, - with_tasks_completed_by_my_group, - with_relation_to_me, + process_instance_report=process_instance_report, + process_model_identifier=process_model_identifier, + user_group_identifier=user_group_identifier, + start_from=start_from, + start_to=start_to, + end_from=end_from, + end_to=end_to, + process_status=process_status, + initiated_by_me=initiated_by_me, + with_tasks_completed_by_me=with_tasks_completed_by_me, + with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, + with_relation_to_me=with_relation_to_me, ) ) - process_instance_query = ProcessInstanceModel.query - # Always join that hot user table for good performance at serialization time. - process_instance_query = process_instance_query.options( - selectinload(ProcessInstanceModel.process_initiator) - ) - - if report_filter.process_model_identifier is not None: - process_model = get_process_model( - f"{report_filter.process_model_identifier}", - ) - - process_instance_query = process_instance_query.filter_by( - process_model_identifier=process_model.id - ) - - # this can never happen. obviously the class has the columns it defines. this is just to appease mypy. - if ( - ProcessInstanceModel.start_in_seconds is None - or ProcessInstanceModel.end_in_seconds is None - ): - raise ( - ApiError( - error_code="unexpected_condition", - message="Something went very wrong", - status_code=500, - ) - ) - - if report_filter.start_from is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.start_in_seconds >= report_filter.start_from - ) - if report_filter.start_to is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.start_in_seconds <= report_filter.start_to - ) - if report_filter.end_from is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.end_in_seconds >= report_filter.end_from - ) - if report_filter.end_to is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.end_in_seconds <= report_filter.end_to - ) - if report_filter.process_status is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore - ) - - if report_filter.initiated_by_me is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - process_instance_query = process_instance_query.filter_by( - process_initiator=g.user - ) - - if report_filter.with_relation_to_me is True: - process_instance_query = process_instance_query.outerjoin( - HumanTaskModel - ).outerjoin( - HumanTaskUserModel, - and_( - HumanTaskModel.id == HumanTaskUserModel.human_task_id, - HumanTaskUserModel.user_id == g.user.id, - ), - ) - process_instance_query = process_instance_query.filter( - or_( - HumanTaskUserModel.id.is_not(None), - ProcessInstanceModel.process_initiator_id == g.user.id, - ) - ) - - # TODO: not sure if this is exactly what is wanted - if report_filter.with_tasks_completed_by_me is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - # process_instance_query = process_instance_query.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) - # process_instance_query = process_instance_query.add_columns(UserModel.username) - # search for process_instance.UserModel.username in this file for more details about why adding columns is annoying. - - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.process_initiator_id != g.user.id - ) - process_instance_query = process_instance_query.join( - SpiffStepDetailsModel, - ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, - ) - process_instance_query = process_instance_query.join( - SpiffLoggingModel, - ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, - ) - process_instance_query = process_instance_query.filter( - SpiffLoggingModel.message.contains("COMPLETED") # type: ignore - ) - process_instance_query = process_instance_query.filter( - SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step - ) - process_instance_query = process_instance_query.filter( - SpiffStepDetailsModel.completed_by_user_id == g.user.id - ) - - if report_filter.with_tasks_completed_by_my_group is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - process_instance_query = process_instance_query.join( - SpiffStepDetailsModel, - ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, - ) - process_instance_query = process_instance_query.join( - SpiffLoggingModel, - ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, - ) - process_instance_query = process_instance_query.filter( - SpiffLoggingModel.message.contains("COMPLETED") # type: ignore - ) - process_instance_query = process_instance_query.filter( - SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step - ) - if group_identifier: - process_instance_query = process_instance_query.join( - GroupModel, - GroupModel.identifier == group_identifier, - ) - else: - process_instance_query = process_instance_query.join( - GroupModel, - GroupModel.id == SpiffStepDetailsModel.lane_assignment_id, - ) - process_instance_query = process_instance_query.join( - UserGroupAssignmentModel, - UserGroupAssignmentModel.group_id == GroupModel.id, - ) - process_instance_query = process_instance_query.filter( - UserGroupAssignmentModel.user_id == g.user.id - ) + process_instance_query = ProcessInstanceReportService.run_process_instance_report(report_filter, g.user) instance_metadata_aliases = {} stock_columns = ProcessInstanceReportService.get_column_names_for_model( @@ -1470,11 +1333,11 @@ def task_list_for_me(page: int = 1, per_page: int = 100) -> flask.wrappers.Respo def task_list_for_my_groups( - group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100 + user_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100 ) -> flask.wrappers.Response: """Task_list_for_my_groups.""" return get_tasks( - group_identifier=group_identifier, + user_group_identifier=user_group_identifier, processes_started_by_user=False, page=page, per_page=per_page, @@ -1494,7 +1357,7 @@ def get_tasks( has_lane_assignment_id: bool = True, page: int = 1, per_page: int = 100, - group_identifier: Optional[str] = None, + user_group_identifier: Optional[str] = None, ) -> flask.wrappers.Response: """Get_tasks.""" user_id = g.user.id @@ -1532,9 +1395,9 @@ def get_tasks( ), ) if has_lane_assignment_id: - if group_identifier: + if user_group_identifier: human_tasks_query = human_tasks_query.filter( - GroupModel.identifier == group_identifier + GroupModel.identifier == user_group_identifier ) else: human_tasks_query = human_tasks_query.filter( @@ -1550,7 +1413,7 @@ def get_tasks( ProcessInstanceModel.updated_at_in_seconds, ProcessInstanceModel.created_at_in_seconds, UserModel.username, - GroupModel.identifier.label("group_identifier"), + GroupModel.identifier.label("user_group_identifier"), HumanTaskModel.task_name, HumanTaskModel.task_title, HumanTaskModel.process_model_display_name, 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 c06f49a3..753e3cd0 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -558,7 +558,7 @@ class ProcessInstanceProcessor: "spiff_step": self.process_instance_model.spiff_step or 1, "task_json": task_json, "timestamp": round(time.time()), - "completed_by_user_id": self.current_user().id, + # "completed_by_user_id": self.current_user().id, } def spiff_step_details(self) -> SpiffStepDetailsModel: @@ -569,14 +569,14 @@ class ProcessInstanceProcessor: spiff_step=details_mapping["spiff_step"], task_json=details_mapping["task_json"], timestamp=details_mapping["timestamp"], - completed_by_user_id=details_mapping["completed_by_user_id"], + # completed_by_user_id=details_mapping["completed_by_user_id"], ) return details_model def save_spiff_step_details(self, human_task: HumanTaskModel) -> None: """SaveSpiffStepDetails.""" details_model = self.spiff_step_details() - details_model.lane_assignment_id = human_task.lane_assignment_id + # details_model.lane_assignment_id = human_task.lane_assignment_id db.session.add(details_model) db.session.commit() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index e0595f3f..e9cfdd07 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -1,5 +1,16 @@ """Process_instance_report_service.""" from dataclasses import dataclass +from sqlalchemy import or_ +from sqlalchemy import and_ +from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel +from spiffworkflow_backend.models.human_task import HumanTaskModel +from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel +from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel +from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel +from spiffworkflow_backend.models.group import GroupModel +from flask_bpmn.api.api_error import ApiError +from sqlalchemy.orm import selectinload +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from typing import Optional import sqlalchemy @@ -9,6 +20,7 @@ from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, ) from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_model_service import ProcessModelService @dataclass @@ -16,6 +28,7 @@ class ProcessInstanceReportFilter: """ProcessInstanceReportFilter.""" process_model_identifier: Optional[str] = None + user_group_identifier: Optional[str] = None start_from: Optional[int] = None start_to: Optional[int] = None end_from: Optional[int] = None @@ -32,6 +45,8 @@ class ProcessInstanceReportFilter: if self.process_model_identifier is not None: d["process_model_identifier"] = self.process_model_identifier + if self.user_group_identifier is not None: + d["user_group_identifier"] = self.user_group_identifier if self.start_from is not None: d["start_from"] = str(self.start_from) if self.start_to is not None: @@ -167,6 +182,7 @@ class ProcessInstanceReportService: return filters[key].split(",") if key in filters else None process_model_identifier = filters.get("process_model_identifier") + user_group_identifier = filters.get("user_group_identifier") start_from = int_value("start_from") start_to = int_value("start_to") end_from = int_value("end_from") @@ -181,6 +197,7 @@ class ProcessInstanceReportService: report_filter = ProcessInstanceReportFilter( process_model_identifier, + user_group_identifier, start_from, start_to, end_from, @@ -199,6 +216,7 @@ class ProcessInstanceReportService: cls, process_instance_report: ProcessInstanceReportModel, process_model_identifier: Optional[str] = None, + user_group_identifier: Optional[str] = None, start_from: Optional[int] = None, start_to: Optional[int] = None, end_from: Optional[int] = None, @@ -214,6 +232,8 @@ class ProcessInstanceReportService: if process_model_identifier is not None: report_filter.process_model_identifier = process_model_identifier + if user_group_identifier is not None: + report_filter.user_group_identifier = user_group_identifier if start_from is not None: report_filter.start_from = start_from if start_to is not None: @@ -276,3 +296,169 @@ class ProcessInstanceReportService: {"Header": "Username", "accessor": "username", "filterable": False}, {"Header": "Status", "accessor": "status", "filterable": False}, ] + + @classmethod + def run_process_instance_report(cls, report_filter: ProcessInstanceReportFilter, user: UserModel) -> None: + process_instance_query = ProcessInstanceModel.query + # Always join that hot user table for good performance at serialization time. + process_instance_query = process_instance_query.options( + selectinload(ProcessInstanceModel.process_initiator) + ) + + if report_filter.process_model_identifier is not None: + process_model = ProcessModelService.get_process_model( + f"{report_filter.process_model_identifier}", + ) + + process_instance_query = process_instance_query.filter_by( + process_model_identifier=process_model.id + ) + + # this can never happen. obviously the class has the columns it defines. this is just to appease mypy. + if ( + ProcessInstanceModel.start_in_seconds is None + or ProcessInstanceModel.end_in_seconds is None + ): + raise ( + ApiError( + error_code="unexpected_condition", + message="Something went very wrong", + status_code=500, + ) + ) + + if report_filter.start_from is not None: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.start_in_seconds >= report_filter.start_from + ) + if report_filter.start_to is not None: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.start_in_seconds <= report_filter.start_to + ) + if report_filter.end_from is not None: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.end_in_seconds >= report_filter.end_from + ) + if report_filter.end_to is not None: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.end_in_seconds <= report_filter.end_to + ) + if report_filter.process_status is not None: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore + ) + + if report_filter.initiated_by_me is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore + ) + process_instance_query = process_instance_query.filter_by( + process_initiator=user + ) + + if report_filter.with_relation_to_me is True: + process_instance_query = process_instance_query.outerjoin( + HumanTaskModel + ).outerjoin( + HumanTaskUserModel, + and_( + HumanTaskModel.id == HumanTaskUserModel.human_task_id, + HumanTaskUserModel.user_id == user.id, + ), + ) + process_instance_query = process_instance_query.filter( + or_( + HumanTaskUserModel.id.is_not(None), + ProcessInstanceModel.process_initiator_id == user.id, + ) + ) + + # TODO: not sure if this is exactly what is wanted + if report_filter.with_tasks_completed_by_me is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore + ) + # process_instance_query = process_instance_query.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) + # process_instance_query = process_instance_query.add_columns(UserModel.username) + # search for process_instance.UserModel.username in this file for more details about why adding columns is annoying. + + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.process_initiator_id != user.id + ) + process_instance_query = process_instance_query.join( + HumanTaskModel, + and_( + HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + HumanTaskModel.completed_by_user_id == user.id + ) + ) + # ).join( + # HumanTaskUserModel, + # and_( + # HumanTaskModel.id == HumanTaskUserModel.human_task_id, + # HumanTaskUserModel.user_id == user.id, + # ), + # ) + # process_instance_query = process_instance_query.join( + # SpiffStepDetailsModel, + # ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, + # ) + # process_instance_query = process_instance_query.join( + # SpiffLoggingModel, + # ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, + # ) + # process_instance_query = process_instance_query.filter( + # SpiffLoggingModel.message.contains("COMPLETED") # type: ignore + # ) + # process_instance_query = process_instance_query.filter( + # SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step + # ) + # process_instance_query = process_instance_query.filter( + # HumanTaskModel.completed_by_user_id == user.id + # ) + + if report_filter.with_tasks_completed_by_my_group is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore + ) + # process_instance_query = process_instance_query.join( + # SpiffStepDetailsModel, + # ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, + # ) + # process_instance_query = process_instance_query.join( + # SpiffLoggingModel, + # ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, + # ) + # process_instance_query = process_instance_query.filter( + # SpiffLoggingModel.message.contains("COMPLETED") # type: ignore + # ) + # process_instance_query = process_instance_query.filter( + # SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step + # ) + if report_filter.user_group_identifier: + process_instance_query = process_instance_query.join( + GroupModel, + GroupModel.identifier == report_filter.user_group_identifier, + ) + else: + process_instance_query = process_instance_query.join( + HumanTaskModel + ).join( + HumanTaskUserModel, + and_( + HumanTaskModel.id == HumanTaskUserModel.human_task_id, + HumanTaskUserModel.user_id == user.id, + ), + ) + process_instance_query = process_instance_query.join( + GroupModel, + GroupModel.id == HumanTaskModel.lane_assignment_id, + ) + process_instance_query = process_instance_query.join( + UserGroupAssignmentModel, + UserGroupAssignmentModel.group_id == GroupModel.id, + ) + process_instance_query = process_instance_query.filter( + UserGroupAssignmentModel.user_id == user.id + ) + return process_instance_query diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py index 98412faa..318a0c8d 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py @@ -1,8 +1,11 @@ """Test_process_instance_report_service.""" from typing import Optional +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask import Flask from flask.testing import FlaskClient +from spiffworkflow_backend.models import process_instance_report from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.process_instance_report import ( @@ -122,13 +125,13 @@ class TestProcessInstanceReportService(BaseTest): report_metadata=report_metadata, ) return ProcessInstanceReportService.filter_from_metadata_with_overrides( - report, - process_model_identifier, - start_from, - start_to, - end_from, - end_to, - process_status, + process_instance_report=report, + process_model_identifier=process_model_identifier, + start_from=start_from, + start_to=start_to, + end_from=end_from, + end_to=end_to, + process_status=process_status, ) def _filter_by_dict_from_metadata(self, report_metadata: dict) -> dict[str, str]: @@ -743,3 +746,20 @@ class TestProcessInstanceReportService(BaseTest): assert report_filter.end_from is None assert report_filter.end_to is None assert report_filter.process_status == ["sue"] + + def test_can_filter_by_with_relation_to_me( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + process_model_id = "runs_without_input/sample" + bpmn_file_location = "sample" + load_test_spec( + process_model_id, + process_model_source_directory=bpmn_file_location, + ) + user_one = self.find_or_create_user(username="user_one") + user_two = self.find_or_create_user(username="user_two") + + process_instance_created_by_user_one_one = ProcessInstanceModel() From fd60c3566c6387f05bc33e2fb427b3f11a74b1ff Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 11:19:04 -0500 Subject: [PATCH 2/9] renamed test process instance create method w/ burnettk --- .../helpers/base_test.py | 2 +- .../integration/test_logging_service.py | 2 +- .../integration/test_nested_groups.py | 4 +-- .../integration/test_process_api.py | 36 +++++++++---------- .../unit/test_dot_notation.py | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 48982fc6..52f1889e 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -243,7 +243,7 @@ class BaseTest: return file @staticmethod - def create_process_instance_from_process_model_id( + def create_process_instance_from_process_model_id_with_api( client: FlaskClient, test_process_model_id: str, headers: Dict[str, str], diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py index f9dd4452..d27bbdc7 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py @@ -45,7 +45,7 @@ class TestLoggingService(BaseTest): user=with_super_admin_user, ) headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) assert response.json is not None diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py index 3983f9be..90b5af88 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py @@ -38,7 +38,7 @@ class TestNestedGroups(BaseTest): bpmn_file_name=bpmn_file_name, bpmn_file_location=bpmn_file_location, ) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, self.logged_in_headers(with_super_admin_user), @@ -99,7 +99,7 @@ class TestNestedGroups(BaseTest): bpmn_file_name=bpmn_file_name, bpmn_file_location=bpmn_file_location, ) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, self.logged_in_headers(with_super_admin_user), 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 9c763b58..adc21c29 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -284,7 +284,7 @@ class TestProcessApi(BaseTest): ) headers = self.logged_in_headers(with_super_admin_user) # create an instance from a model - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) @@ -1072,7 +1072,7 @@ class TestProcessApi(BaseTest): """Test_process_instance_create.""" test_process_model_id = "runs_without_input/sample" headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, test_process_model_id, headers ) assert response.json is not None @@ -1102,7 +1102,7 @@ class TestProcessApi(BaseTest): ) headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) assert response.json is not None @@ -1144,7 +1144,7 @@ class TestProcessApi(BaseTest): self.modify_process_identifier_for_path_param(process_model_identifier) ) headers = self.logged_in_headers(with_super_admin_user) - create_response = self.create_process_instance_from_process_model_id( + create_response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) assert create_response.json is not None @@ -1191,7 +1191,7 @@ class TestProcessApi(BaseTest): self.modify_process_identifier_for_path_param(process_model_identifier) ) headers = self.logged_in_headers(with_super_admin_user) - create_response = self.create_process_instance_from_process_model_id( + create_response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) assert create_response.json is not None @@ -1299,7 +1299,7 @@ class TestProcessApi(BaseTest): "andThis": "another_item_non_key", } } - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, self.logged_in_headers(with_super_admin_user), @@ -1359,7 +1359,7 @@ class TestProcessApi(BaseTest): bpmn_file_location=bpmn_file_location, ) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, self.logged_in_headers(with_super_admin_user), @@ -1407,7 +1407,7 @@ class TestProcessApi(BaseTest): ) headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) assert response.json is not None @@ -1448,7 +1448,7 @@ class TestProcessApi(BaseTest): ) headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) assert response.json is not None @@ -1499,7 +1499,7 @@ class TestProcessApi(BaseTest): ) headers = self.logged_in_headers(with_super_admin_user) - self.create_process_instance_from_process_model_id( + self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) @@ -1546,19 +1546,19 @@ class TestProcessApi(BaseTest): bpmn_file_location=bpmn_file_location, ) headers = self.logged_in_headers(with_super_admin_user) - self.create_process_instance_from_process_model_id( + self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) - self.create_process_instance_from_process_model_id( + self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) - self.create_process_instance_from_process_model_id( + self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) - self.create_process_instance_from_process_model_id( + self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) - self.create_process_instance_from_process_model_id( + self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) @@ -1872,7 +1872,7 @@ class TestProcessApi(BaseTest): ) -> Any: """Setup_testing_instance.""" headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_id, headers ) process_instance = response.json @@ -2195,7 +2195,7 @@ class TestProcessApi(BaseTest): # process_group_id="finance", # ) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, # process_model.process_group_id, process_model_identifier, @@ -2404,7 +2404,7 @@ class TestProcessApi(BaseTest): ) headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) assert response.json is not None 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 c84ec470..59a0fee8 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py @@ -37,7 +37,7 @@ class TestDotNotation(BaseTest): ) headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id( + response = self.create_process_instance_from_process_model_id_with_api( client, process_model_identifier, headers ) process_instance_id = response.json["id"] From 8f2bc4c8a4dec0722d5d0954b50151f19565e102 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 12:29:14 -0500 Subject: [PATCH 3/9] added test for report filters w/ burnettk --- .../routes/process_api_blueprint.py | 111 ++-------- .../process_instance_report_service.py | 206 +++++++++++------- .../test_process_instance_report_service.py | 29 ++- .../src/components/MyCompletedInstances.tsx | 2 +- .../src/routes/CompletedInstances.tsx | 6 +- 5 files changed, 175 insertions(+), 179 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 9afba606..de45daff 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -889,10 +889,10 @@ def process_instance_list( end_from: Optional[int] = None, end_to: Optional[int] = None, process_status: Optional[str] = None, - initiated_by_me: Optional[bool] = None, - with_tasks_completed_by_me: Optional[bool] = None, - with_tasks_completed_by_my_group: Optional[bool] = None, - with_relation_to_me: Optional[bool] = None, + # initiated_by_me: Optional[bool] = None, + # with_tasks_completed_by_me: Optional[bool] = None, + # with_tasks_completed_by_my_group: Optional[bool] = None, + # with_relation_to_me: Optional[bool] = None, user_filter: Optional[bool] = False, report_identifier: Optional[str] = None, report_id: Optional[int] = None, @@ -911,10 +911,10 @@ def process_instance_list( start_to=start_to, end_from=end_from, end_to=end_to, - initiated_by_me=initiated_by_me, - with_tasks_completed_by_me=with_tasks_completed_by_me, - with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, - with_relation_to_me=with_relation_to_me, + # initiated_by_me=initiated_by_me, + # with_tasks_completed_by_me=with_tasks_completed_by_me, + # with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, + # with_relation_to_me=with_relation_to_me, process_status=process_status.split(",") if process_status else None, ) else: @@ -928,95 +928,20 @@ def process_instance_list( end_from=end_from, end_to=end_to, process_status=process_status, - initiated_by_me=initiated_by_me, - with_tasks_completed_by_me=with_tasks_completed_by_me, - with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, - with_relation_to_me=with_relation_to_me, + # initiated_by_me=initiated_by_me, + # with_tasks_completed_by_me=with_tasks_completed_by_me, + # with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, + # with_relation_to_me=with_relation_to_me, ) ) - process_instance_query = ProcessInstanceReportService.run_process_instance_report(report_filter, g.user) - - instance_metadata_aliases = {} - stock_columns = ProcessInstanceReportService.get_column_names_for_model( - ProcessInstanceModel + response_json = ProcessInstanceReportService.run_process_instance_report( + report_filter=report_filter, + process_instance_report=process_instance_report, + page=page, + per_page=per_page, + user=g.user ) - for column in process_instance_report.report_metadata["columns"]: - if column["accessor"] in stock_columns: - continue - instance_metadata_alias = aliased(ProcessInstanceMetadataModel) - instance_metadata_aliases[column["accessor"]] = instance_metadata_alias - - filter_for_column = None - if "filter_by" in process_instance_report.report_metadata: - filter_for_column = next( - ( - f - for f in process_instance_report.report_metadata["filter_by"] - if f["field_name"] == column["accessor"] - ), - None, - ) - isouter = True - conditions = [ - ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, - instance_metadata_alias.key == column["accessor"], - ] - if filter_for_column: - isouter = False - conditions.append( - instance_metadata_alias.value == filter_for_column["field_value"] - ) - process_instance_query = process_instance_query.join( - instance_metadata_alias, and_(*conditions), isouter=isouter - ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) - - order_by_query_array = [] - order_by_array = process_instance_report.report_metadata["order_by"] - if len(order_by_array) < 1: - order_by_array = ProcessInstanceReportModel.default_order_by() - for order_by_option in order_by_array: - attribute = re.sub("^-", "", order_by_option) - if attribute in stock_columns: - if order_by_option.startswith("-"): - order_by_query_array.append( - getattr(ProcessInstanceModel, attribute).desc() - ) - else: - order_by_query_array.append( - getattr(ProcessInstanceModel, attribute).asc() - ) - elif attribute in instance_metadata_aliases: - if order_by_option.startswith("-"): - order_by_query_array.append( - func.max(instance_metadata_aliases[attribute].value).desc() - ) - else: - order_by_query_array.append( - func.max(instance_metadata_aliases[attribute].value).asc() - ) - - process_instances = ( - process_instance_query.group_by(ProcessInstanceModel.id) - .add_columns(ProcessInstanceModel.id) - .order_by(*order_by_query_array) - .paginate(page=page, per_page=per_page, error_out=False) - ) - - results = ProcessInstanceReportService.add_metadata_columns_to_process_instance( - process_instances.items, process_instance_report.report_metadata["columns"] - ) - - response_json = { - "report": process_instance_report, - "results": results, - "filters": report_filter.to_dict(), - "pagination": { - "count": len(results), - "total": process_instances.total, - "pages": process_instances.pages, - }, - } return make_response(jsonify(response_json), 200) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index e9cfdd07..bcf7fb41 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -1,9 +1,14 @@ """Process_instance_report_service.""" from dataclasses import dataclass +from sqlalchemy.orm import relationship +from sqlalchemy import func +from sqlalchemy.orm import aliased from sqlalchemy import or_ from sqlalchemy import and_ +import re from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel from spiffworkflow_backend.models.human_task import HumanTaskModel +from spiffworkflow_backend.models.process_instance_metadata import ProcessInstanceMetadataModel from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel @@ -35,8 +40,9 @@ class ProcessInstanceReportFilter: end_to: Optional[int] = None process_status: Optional[list[str]] = None initiated_by_me: Optional[bool] = None + has_terminal_status: Optional[bool] = None with_tasks_completed_by_me: Optional[bool] = None - with_tasks_completed_by_my_group: Optional[bool] = None + with_tasks_assigned_to_my_group: Optional[bool] = None with_relation_to_me: Optional[bool] = None def to_dict(self) -> dict[str, str]: @@ -59,13 +65,15 @@ class ProcessInstanceReportFilter: d["process_status"] = ",".join(self.process_status) if self.initiated_by_me is not None: d["initiated_by_me"] = str(self.initiated_by_me).lower() + if self.has_terminal_status is not None: + d["has_terminal_status"] = str(self.has_terminal_status).lower() if self.with_tasks_completed_by_me is not None: d["with_tasks_completed_by_me"] = str( self.with_tasks_completed_by_me ).lower() - if self.with_tasks_completed_by_my_group is not None: - d["with_tasks_completed_by_my_group"] = str( - self.with_tasks_completed_by_my_group + if self.with_tasks_assigned_to_my_group is not None: + d["with_tasks_assigned_to_my_group"] = str( + self.with_tasks_assigned_to_my_group ).lower() if self.with_relation_to_me is not None: d["with_relation_to_me"] = str(self.with_relation_to_me).lower() @@ -107,7 +115,7 @@ class ProcessInstanceReportService: "filter_by": [], "order_by": ["-start_in_seconds", "-id"], }, - "system_report_instances_initiated_by_me": { + "system_report_completed_instances_initiated_by_me": { "columns": [ {"Header": "id", "accessor": "id"}, { @@ -118,23 +126,25 @@ class ProcessInstanceReportService: {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, {"Header": "status", "accessor": "status"}, ], - "filter_by": [{"field_name": "initiated_by_me", "field_value": True}], + "filter_by": [{"field_name": "initiated_by_me", "field_value": True}, {"field_name": "has_terminal_status", "field_value": True}], "order_by": ["-start_in_seconds", "-id"], }, - "system_report_instances_with_tasks_completed_by_me": { + "system_report_completed_instances_with_tasks_completed_by_me": { "columns": cls.builtin_column_options(), "filter_by": [ - {"field_name": "with_tasks_completed_by_me", "field_value": True} + {"field_name": "with_tasks_completed_by_me", "field_value": True}, + {"field_name": "has_terminal_status", "field_value": True} ], "order_by": ["-start_in_seconds", "-id"], }, - "system_report_instances_with_tasks_completed_by_my_groups": { + "system_report_completed_instances_with_tasks_completed_by_my_groups": { "columns": cls.builtin_column_options(), "filter_by": [ { - "field_name": "with_tasks_completed_by_my_group", + "field_name": "with_tasks_assigned_to_my_group", "field_value": True, - } + }, + {"field_name": "has_terminal_status", "field_value": True} ], "order_by": ["-start_in_seconds", "-id"], }, @@ -189,9 +199,10 @@ class ProcessInstanceReportService: end_to = int_value("end_to") process_status = list_value("process_status") initiated_by_me = bool_value("initiated_by_me") + has_terminal_status = bool_value("has_terminal_status") with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") - with_tasks_completed_by_my_group = bool_value( - "with_tasks_completed_by_my_group" + with_tasks_assigned_to_my_group = bool_value( + "with_tasks_assigned_to_my_group" ) with_relation_to_me = bool_value("with_relation_to_me") @@ -204,8 +215,9 @@ class ProcessInstanceReportService: end_to, process_status, initiated_by_me, + has_terminal_status, with_tasks_completed_by_me, - with_tasks_completed_by_my_group, + with_tasks_assigned_to_my_group, with_relation_to_me, ) @@ -223,8 +235,9 @@ class ProcessInstanceReportService: end_to: Optional[int] = None, process_status: Optional[str] = None, initiated_by_me: Optional[bool] = None, + has_terminal_status: Optional[bool] = None, with_tasks_completed_by_me: Optional[bool] = None, - with_tasks_completed_by_my_group: Optional[bool] = None, + with_tasks_assigned_to_my_group: Optional[bool] = None, with_relation_to_me: Optional[bool] = None, ) -> ProcessInstanceReportFilter: """Filter_from_metadata_with_overrides.""" @@ -246,11 +259,13 @@ class ProcessInstanceReportService: report_filter.process_status = process_status.split(",") if initiated_by_me is not None: report_filter.initiated_by_me = initiated_by_me + if has_terminal_status is not None: + report_filter.has_terminal_status = has_terminal_status if with_tasks_completed_by_me is not None: report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me - if with_tasks_completed_by_my_group is not None: - report_filter.with_tasks_completed_by_my_group = ( - with_tasks_completed_by_my_group + if with_tasks_assigned_to_my_group is not None: + report_filter.with_tasks_assigned_to_my_group = ( + with_tasks_assigned_to_my_group ) if with_relation_to_me is not None: report_filter.with_relation_to_me = with_relation_to_me @@ -298,7 +313,14 @@ class ProcessInstanceReportService: ] @classmethod - def run_process_instance_report(cls, report_filter: ProcessInstanceReportFilter, user: UserModel) -> None: + def run_process_instance_report( + cls, + report_filter: ProcessInstanceReportFilter, + process_instance_report: ProcessInstanceReportModel, + user: UserModel, + page: int = 1, + per_page: int = 100, + ) -> dict: process_instance_query = ProcessInstanceModel.query # Always join that hot user table for good performance at serialization time. process_instance_query = process_instance_query.options( @@ -349,13 +371,15 @@ class ProcessInstanceReportService: ) if report_filter.initiated_by_me is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) process_instance_query = process_instance_query.filter_by( process_initiator=user ) + if report_filter.has_terminal_status is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore + ) + if report_filter.with_relation_to_me is True: process_instance_query = process_instance_query.outerjoin( HumanTaskModel @@ -373,15 +397,7 @@ class ProcessInstanceReportService: ) ) - # TODO: not sure if this is exactly what is wanted if report_filter.with_tasks_completed_by_me is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - # process_instance_query = process_instance_query.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) - # process_instance_query = process_instance_query.add_columns(UserModel.username) - # search for process_instance.UserModel.username in this file for more details about why adding columns is annoying. - process_instance_query = process_instance_query.filter( ProcessInstanceModel.process_initiator_id != user.id ) @@ -392,49 +408,8 @@ class ProcessInstanceReportService: HumanTaskModel.completed_by_user_id == user.id ) ) - # ).join( - # HumanTaskUserModel, - # and_( - # HumanTaskModel.id == HumanTaskUserModel.human_task_id, - # HumanTaskUserModel.user_id == user.id, - # ), - # ) - # process_instance_query = process_instance_query.join( - # SpiffStepDetailsModel, - # ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, - # ) - # process_instance_query = process_instance_query.join( - # SpiffLoggingModel, - # ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, - # ) - # process_instance_query = process_instance_query.filter( - # SpiffLoggingModel.message.contains("COMPLETED") # type: ignore - # ) - # process_instance_query = process_instance_query.filter( - # SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step - # ) - # process_instance_query = process_instance_query.filter( - # HumanTaskModel.completed_by_user_id == user.id - # ) - if report_filter.with_tasks_completed_by_my_group is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - # process_instance_query = process_instance_query.join( - # SpiffStepDetailsModel, - # ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, - # ) - # process_instance_query = process_instance_query.join( - # SpiffLoggingModel, - # ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, - # ) - # process_instance_query = process_instance_query.filter( - # SpiffLoggingModel.message.contains("COMPLETED") # type: ignore - # ) - # process_instance_query = process_instance_query.filter( - # SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step - # ) + if report_filter.with_tasks_assigned_to_my_group is True: if report_filter.user_group_identifier: process_instance_query = process_instance_query.join( GroupModel, @@ -443,12 +418,6 @@ class ProcessInstanceReportService: else: process_instance_query = process_instance_query.join( HumanTaskModel - ).join( - HumanTaskUserModel, - and_( - HumanTaskModel.id == HumanTaskUserModel.human_task_id, - HumanTaskUserModel.user_id == user.id, - ), ) process_instance_query = process_instance_query.join( GroupModel, @@ -461,4 +430,83 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter( UserGroupAssignmentModel.user_id == user.id ) - return process_instance_query + + instance_metadata_aliases = {} + stock_columns = ProcessInstanceReportService.get_column_names_for_model( + ProcessInstanceModel + ) + for column in process_instance_report.report_metadata["columns"]: + if column["accessor"] in stock_columns: + continue + instance_metadata_alias = aliased(ProcessInstanceMetadataModel) + instance_metadata_aliases[column["accessor"]] = instance_metadata_alias + + filter_for_column = None + if "filter_by" in process_instance_report.report_metadata: + filter_for_column = next( + ( + f + for f in process_instance_report.report_metadata["filter_by"] + if f["field_name"] == column["accessor"] + ), + None, + ) + isouter = True + conditions = [ + ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, + instance_metadata_alias.key == column["accessor"], + ] + if filter_for_column: + isouter = False + conditions.append( + instance_metadata_alias.value == filter_for_column["field_value"] + ) + process_instance_query = process_instance_query.join( + instance_metadata_alias, and_(*conditions), isouter=isouter + ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) + + order_by_query_array = [] + order_by_array = process_instance_report.report_metadata["order_by"] + if len(order_by_array) < 1: + order_by_array = ProcessInstanceReportModel.default_order_by() + for order_by_option in order_by_array: + attribute = re.sub("^-", "", order_by_option) + if attribute in stock_columns: + if order_by_option.startswith("-"): + order_by_query_array.append( + getattr(ProcessInstanceModel, attribute).desc() + ) + else: + order_by_query_array.append( + getattr(ProcessInstanceModel, attribute).asc() + ) + elif attribute in instance_metadata_aliases: + if order_by_option.startswith("-"): + order_by_query_array.append( + func.max(instance_metadata_aliases[attribute].value).desc() + ) + else: + order_by_query_array.append( + func.max(instance_metadata_aliases[attribute].value).asc() + ) + # return process_instance_query + process_instances = ( + process_instance_query.group_by(ProcessInstanceModel.id) + .add_columns(ProcessInstanceModel.id) + .order_by(*order_by_query_array) + .paginate(page=page, per_page=per_page, error_out=False) + ) + results = ProcessInstanceReportService.add_metadata_columns_to_process_instance( + process_instances.items, process_instance_report.report_metadata["columns"] + ) + response_json = { + "report": process_instance_report, + "results": results, + "filters": report_filter.to_dict(), + "pagination": { + "count": len(results), + "total": process_instances.total, + "pages": process_instances.pages, + }, + } + return response_json diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py index 318a0c8d..9d08e637 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py @@ -747,7 +747,7 @@ class TestProcessInstanceReportService(BaseTest): assert report_filter.end_to is None assert report_filter.process_status == ["sue"] - def test_can_filter_by_with_relation_to_me( + def test_can_filter_by_initiated_by_me( self, app: Flask, client: FlaskClient, @@ -755,11 +755,34 @@ class TestProcessInstanceReportService(BaseTest): ) -> None: process_model_id = "runs_without_input/sample" bpmn_file_location = "sample" - load_test_spec( + process_model = load_test_spec( process_model_id, process_model_source_directory=bpmn_file_location, ) user_one = self.find_or_create_user(username="user_one") user_two = self.find_or_create_user(username="user_two") - process_instance_created_by_user_one_one = ProcessInstanceModel() + _process_instance_created_by_user_one_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) + _process_instance_created_by_user_one_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) + _process_instance_created_by_user_one_three = self.create_process_instance_from_process_model(process_model=process_model, status="waiting", user=user_one) + _process_instance_created_by_user_two_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) + _process_instance_created_by_user_two_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) + + process_instance_report = ProcessInstanceReportService.report_with_identifier( + user=user_one, report_identifier="system_report_completed_instances_initiated_by_me" + ) + report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( + process_instance_report=process_instance_report, + process_model_identifier=process_model.id, + ) + response_json = ProcessInstanceReportService.run_process_instance_report( + report_filter=report_filter, + process_instance_report=process_instance_report, + user=user_one + ) + + assert len(response_json['results']) == 2 + assert response_json['results'][0]['process_initiator_id'] == user_one.id + assert response_json['results'][1]['process_initiator_id'] == user_one.id + assert response_json['results'][0]['status'] == 'complete' + assert response_json['results'][1]['status'] == 'complete' diff --git a/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx b/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx index 2d0fe26a..47042e91 100644 --- a/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx +++ b/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx @@ -8,7 +8,7 @@ export default function MyCompletedInstances() { filtersEnabled={false} paginationQueryParamPrefix={paginationQueryParamPrefix} perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_instances_initiated_by_me" + reportIdentifier="system_report_completed_instances_initiated_by_me" showReports={false} /> ); diff --git a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx index 361692b0..c9528e93 100644 --- a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx +++ b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx @@ -30,7 +30,7 @@ export default function CompletedInstances() { paginationQueryParamPrefix="group_completed_instances" paginationClassName="with-large-bottom-margin" perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_instances_with_tasks_completed_by_my_groups" + reportIdentifier="system_report_completed_instances_with_tasks_completed_by_my_groups" showReports={false} textToShowIfEmpty="This group has no completed instances at this time." additionalParams={`group_identifier=${userGroup}`} @@ -50,7 +50,7 @@ export default function CompletedInstances() { filtersEnabled={false} paginationQueryParamPrefix="my_completed_instances" perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_instances_initiated_by_me" + reportIdentifier="system_report_completed_instances_initiated_by_me" showReports={false} textToShowIfEmpty="You have no completed instances at this time." paginationClassName="with-large-bottom-margin" @@ -64,7 +64,7 @@ export default function CompletedInstances() { filtersEnabled={false} paginationQueryParamPrefix="my_completed_tasks" perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_instances_with_tasks_completed_by_me" + reportIdentifier="system_report_completed_instances_with_tasks_completed_by_me" showReports={false} textToShowIfEmpty="You have no completed instances at this time." paginationClassName="with-large-bottom-margin" From fd9506007ca876e2f52664943197d91af15cf5b9 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 13:50:09 -0500 Subject: [PATCH 4/9] added another filter test --- .../process_instance_report_service.py | 1 - .../test_process_instance_report_service.py | 78 ++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index bcf7fb41..d6932c31 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -149,7 +149,6 @@ class ProcessInstanceReportService: "order_by": ["-start_in_seconds", "-id"], }, } - process_instance_report = ProcessInstanceReportModel( identifier=report_identifier, created_by_id=user.id, diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py index 9d08e637..513910b4 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py @@ -1,5 +1,7 @@ """Test_process_instance_report_service.""" from typing import Optional +from flask_bpmn.models.db import db +from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from tests.spiffworkflow_backend.helpers.test_data import load_test_spec @@ -747,7 +749,7 @@ class TestProcessInstanceReportService(BaseTest): assert report_filter.end_to is None assert report_filter.process_status == ["sue"] - def test_can_filter_by_initiated_by_me( + def test_can_filter_by_completed_instances_initiated_by_me( self, app: Flask, client: FlaskClient, @@ -762,6 +764,7 @@ class TestProcessInstanceReportService(BaseTest): user_one = self.find_or_create_user(username="user_one") user_two = self.find_or_create_user(username="user_two") + # Several processes to ensure they do not return in the result _process_instance_created_by_user_one_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) _process_instance_created_by_user_one_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) _process_instance_created_by_user_one_three = self.create_process_instance_from_process_model(process_model=process_model, status="waiting", user=user_one) @@ -786,3 +789,76 @@ class TestProcessInstanceReportService(BaseTest): assert response_json['results'][1]['process_initiator_id'] == user_one.id assert response_json['results'][0]['status'] == 'complete' assert response_json['results'][1]['status'] == 'complete' + + def test_can_filter_by_completed_instances_with_tasks_completed_by_me( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + process_model_id = "runs_without_input/sample" + bpmn_file_location = "sample" + process_model = load_test_spec( + process_model_id, + process_model_source_directory=bpmn_file_location, + ) + user_one = self.find_or_create_user(username="user_one") + user_two = self.find_or_create_user(username="user_two") + + # Several processes to ensure they do not return in the result + process_instance_created_by_user_one_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) + _process_instance_created_by_user_one_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) + process_instance_created_by_user_one_three = self.create_process_instance_from_process_model(process_model=process_model, status="waiting", user=user_one) + process_instance_created_by_user_two_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) + _process_instance_created_by_user_two_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) + process_instance_created_by_user_two_three = self.create_process_instance_from_process_model(process_model=process_model, status="waiting", user=user_two) + + human_task_for_user_one_one = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_one.id, + completed_by_user_id=user_one.id, + ) + human_task_for_user_one_two = HumanTaskModel( + process_instance_id=process_instance_created_by_user_two_one.id, + completed_by_user_id=user_one.id, + ) + human_task_for_user_one_three = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_three.id, + completed_by_user_id=user_one.id, + ) + human_task_for_user_two_one = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_one.id, + completed_by_user_id=user_two.id, + ) + human_task_for_user_two_two = HumanTaskModel( + process_instance_id=process_instance_created_by_user_two_one.id, + completed_by_user_id=user_two.id, + ) + human_task_for_user_two_three = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_three.id, + completed_by_user_id=user_two.id, + ) + db.session.add(human_task_for_user_one_one) + db.session.add(human_task_for_user_one_two) + db.session.add(human_task_for_user_one_three) + db.session.add(human_task_for_user_two_one) + db.session.add(human_task_for_user_two_two) + db.session.add(human_task_for_user_two_three) + db.session.commit() + + process_instance_report = ProcessInstanceReportService.report_with_identifier( + user=user_one, report_identifier="system_report_completed_instances_with_tasks_completed_by_me" + ) + report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( + process_instance_report=process_instance_report, + process_model_identifier=process_model.id, + ) + response_json = ProcessInstanceReportService.run_process_instance_report( + report_filter=report_filter, + process_instance_report=process_instance_report, + user=user_one + ) + + assert len(response_json['results']) == 1 + assert response_json['results'][0]['process_initiator_id'] == user_two.id + assert response_json['results'][0]['id'] == process_instance_created_by_user_two_one.id + assert response_json['results'][0]['status'] == 'complete' From 98cc8fec61f3a249495fa8d45b2cbfadc8f07905 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 14:39:04 -0500 Subject: [PATCH 5/9] added remaining filter tests w/ burnettk --- spiffworkflow-backend/migrations/env.py | 2 - .../models/human_task.py | 1 - .../models/spiff_step_details.py | 3 - .../routes/process_api_blueprint.py | 22 +- .../process_instance_report_service.py | 57 +-- .../test_process_instance_report_service.py | 336 ++++++++++++++++-- 6 files changed, 334 insertions(+), 87 deletions(-) diff --git a/spiffworkflow-backend/migrations/env.py b/spiffworkflow-backend/migrations/env.py index 68feded2..630e381a 100644 --- a/spiffworkflow-backend/migrations/env.py +++ b/spiffworkflow-backend/migrations/env.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - import logging from logging.config import fileConfig diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py index d4b2caf7..940a51fc 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/human_task.py @@ -8,7 +8,6 @@ from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship -from sqlalchemy.orm import RelationshipProperty from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py index 11a5c7a9..11c3aead 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py @@ -1,14 +1,11 @@ """Spiff_step_details.""" from dataclasses import dataclass -from spiffworkflow_backend.models.human_task import HumanTaskModel -from typing import Optional from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel from sqlalchemy import ForeignKey from sqlalchemy.orm import deferred -from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index de45daff..094b14bb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -2,7 +2,6 @@ import json import os import random -import re import string import uuid from typing import Any @@ -32,10 +31,7 @@ from SpiffWorkflow.task import TaskState from sqlalchemy import and_ from sqlalchemy import asc from sqlalchemy import desc -from sqlalchemy import func from sqlalchemy import or_ -from sqlalchemy.orm import aliased -from sqlalchemy.orm import selectinload from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, @@ -79,7 +75,6 @@ from spiffworkflow_backend.models.spec_reference import SpecReferenceSchema from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.routes.user import verify_token from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService @@ -889,10 +884,7 @@ def process_instance_list( end_from: Optional[int] = None, end_to: Optional[int] = None, process_status: Optional[str] = None, - # initiated_by_me: Optional[bool] = None, - # with_tasks_completed_by_me: Optional[bool] = None, - # with_tasks_completed_by_my_group: Optional[bool] = None, - # with_relation_to_me: Optional[bool] = None, + with_relation_to_me: Optional[bool] = None, user_filter: Optional[bool] = False, report_identifier: Optional[str] = None, report_id: Optional[int] = None, @@ -911,10 +903,7 @@ def process_instance_list( start_to=start_to, end_from=end_from, end_to=end_to, - # initiated_by_me=initiated_by_me, - # with_tasks_completed_by_me=with_tasks_completed_by_me, - # with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, - # with_relation_to_me=with_relation_to_me, + with_relation_to_me=with_relation_to_me, process_status=process_status.split(",") if process_status else None, ) else: @@ -928,10 +917,7 @@ def process_instance_list( end_from=end_from, end_to=end_to, process_status=process_status, - # initiated_by_me=initiated_by_me, - # with_tasks_completed_by_me=with_tasks_completed_by_me, - # with_tasks_completed_by_my_group=with_tasks_completed_by_my_group, - # with_relation_to_me=with_relation_to_me, + with_relation_to_me=with_relation_to_me, ) ) @@ -940,7 +926,7 @@ def process_instance_list( process_instance_report=process_instance_report, page=page, per_page=per_page, - user=g.user + user=g.user, ) return make_response(jsonify(response_json), 200) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index d6932c31..773533ae 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -1,30 +1,29 @@ """Process_instance_report_service.""" -from dataclasses import dataclass -from sqlalchemy.orm import relationship -from sqlalchemy import func -from sqlalchemy.orm import aliased -from sqlalchemy import or_ -from sqlalchemy import and_ import re -from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel -from spiffworkflow_backend.models.human_task import HumanTaskModel -from spiffworkflow_backend.models.process_instance_metadata import ProcessInstanceMetadataModel -from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel -from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel -from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel -from spiffworkflow_backend.models.group import GroupModel -from flask_bpmn.api.api_error import ApiError -from sqlalchemy.orm import selectinload -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from dataclasses import dataclass from typing import Optional import sqlalchemy +from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db +from sqlalchemy import and_ +from sqlalchemy import func +from sqlalchemy import or_ +from sqlalchemy.orm import aliased +from sqlalchemy.orm import selectinload +from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.human_task import HumanTaskModel +from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.models.process_instance_metadata import ( + ProcessInstanceMetadataModel, +) from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, ) from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.services.process_model_service import ProcessModelService @@ -126,14 +125,17 @@ class ProcessInstanceReportService: {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, {"Header": "status", "accessor": "status"}, ], - "filter_by": [{"field_name": "initiated_by_me", "field_value": True}, {"field_name": "has_terminal_status", "field_value": True}], + "filter_by": [ + {"field_name": "initiated_by_me", "field_value": True}, + {"field_name": "has_terminal_status", "field_value": True}, + ], "order_by": ["-start_in_seconds", "-id"], }, "system_report_completed_instances_with_tasks_completed_by_me": { "columns": cls.builtin_column_options(), "filter_by": [ {"field_name": "with_tasks_completed_by_me", "field_value": True}, - {"field_name": "has_terminal_status", "field_value": True} + {"field_name": "has_terminal_status", "field_value": True}, ], "order_by": ["-start_in_seconds", "-id"], }, @@ -144,7 +146,7 @@ class ProcessInstanceReportService: "field_name": "with_tasks_assigned_to_my_group", "field_value": True, }, - {"field_name": "has_terminal_status", "field_value": True} + {"field_name": "has_terminal_status", "field_value": True}, ], "order_by": ["-start_in_seconds", "-id"], }, @@ -200,9 +202,7 @@ class ProcessInstanceReportService: initiated_by_me = bool_value("initiated_by_me") has_terminal_status = bool_value("has_terminal_status") with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") - with_tasks_assigned_to_my_group = bool_value( - "with_tasks_assigned_to_my_group" - ) + with_tasks_assigned_to_my_group = bool_value("with_tasks_assigned_to_my_group") with_relation_to_me = bool_value("with_relation_to_me") report_filter = ProcessInstanceReportFilter( @@ -320,6 +320,7 @@ class ProcessInstanceReportService: page: int = 1, per_page: int = 100, ) -> dict: + """Run_process_instance_report.""" process_instance_query = ProcessInstanceModel.query # Always join that hot user table for good performance at serialization time. process_instance_query = process_instance_query.options( @@ -404,8 +405,8 @@ class ProcessInstanceReportService: HumanTaskModel, and_( HumanTaskModel.process_instance_id == ProcessInstanceModel.id, - HumanTaskModel.completed_by_user_id == user.id - ) + HumanTaskModel.completed_by_user_id == user.id, + ), ) if report_filter.with_tasks_assigned_to_my_group is True: @@ -415,9 +416,7 @@ class ProcessInstanceReportService: GroupModel.identifier == report_filter.user_group_identifier, ) else: - process_instance_query = process_instance_query.join( - HumanTaskModel - ) + process_instance_query = process_instance_query.join(HumanTaskModel) process_instance_query = process_instance_query.join( GroupModel, GroupModel.id == HumanTaskModel.lane_assignment_id, @@ -462,7 +461,9 @@ class ProcessInstanceReportService: ) process_instance_query = process_instance_query.join( instance_metadata_alias, and_(*conditions), isouter=isouter - ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) + ).add_columns( + func.max(instance_metadata_alias.value).label(column["accessor"]) + ) order_by_query_array = [] order_by_array = process_instance_report.report_metadata["order_by"] diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py index 513910b4..75ad3f28 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py @@ -1,15 +1,14 @@ """Test_process_instance_report_service.""" from typing import Optional -from flask_bpmn.models.db import db -from spiffworkflow_backend.models.human_task import HumanTaskModel -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask import Flask from flask.testing import FlaskClient -from spiffworkflow_backend.models import process_instance_report +from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec +from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, ) @@ -20,6 +19,7 @@ from spiffworkflow_backend.services.process_instance_report_service import ( from spiffworkflow_backend.services.process_instance_report_service import ( ProcessInstanceReportService, ) +from spiffworkflow_backend.services.user_service import UserService class TestProcessInstanceReportFilter(BaseTest): @@ -755,6 +755,7 @@ class TestProcessInstanceReportService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: + """Test_can_filter_by_completed_instances_initiated_by_me.""" process_model_id = "runs_without_input/sample" bpmn_file_location = "sample" process_model = load_test_spec( @@ -765,30 +766,43 @@ class TestProcessInstanceReportService(BaseTest): user_two = self.find_or_create_user(username="user_two") # Several processes to ensure they do not return in the result - _process_instance_created_by_user_one_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) - _process_instance_created_by_user_one_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) - _process_instance_created_by_user_one_three = self.create_process_instance_from_process_model(process_model=process_model, status="waiting", user=user_one) - _process_instance_created_by_user_two_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) - _process_instance_created_by_user_two_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="waiting", user=user_one + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) process_instance_report = ProcessInstanceReportService.report_with_identifier( - user=user_one, report_identifier="system_report_completed_instances_initiated_by_me" + user=user_one, + report_identifier="system_report_completed_instances_initiated_by_me", ) - report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=process_instance_report, - process_model_identifier=process_model.id, + report_filter = ( + ProcessInstanceReportService.filter_from_metadata_with_overrides( + process_instance_report=process_instance_report, + process_model_identifier=process_model.id, + ) ) response_json = ProcessInstanceReportService.run_process_instance_report( report_filter=report_filter, process_instance_report=process_instance_report, - user=user_one + user=user_one, ) - assert len(response_json['results']) == 2 - assert response_json['results'][0]['process_initiator_id'] == user_one.id - assert response_json['results'][1]['process_initiator_id'] == user_one.id - assert response_json['results'][0]['status'] == 'complete' - assert response_json['results'][1]['status'] == 'complete' + assert len(response_json["results"]) == 2 + assert response_json["results"][0]["process_initiator_id"] == user_one.id + assert response_json["results"][1]["process_initiator_id"] == user_one.id + assert response_json["results"][0]["status"] == "complete" + assert response_json["results"][1]["status"] == "complete" def test_can_filter_by_completed_instances_with_tasks_completed_by_me( self, @@ -796,6 +810,7 @@ class TestProcessInstanceReportService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: + """Test_can_filter_by_completed_instances_with_tasks_completed_by_me.""" process_model_id = "runs_without_input/sample" bpmn_file_location = "sample" process_model = load_test_spec( @@ -806,12 +821,30 @@ class TestProcessInstanceReportService(BaseTest): user_two = self.find_or_create_user(username="user_two") # Several processes to ensure they do not return in the result - process_instance_created_by_user_one_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) - _process_instance_created_by_user_one_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_one) - process_instance_created_by_user_one_three = self.create_process_instance_from_process_model(process_model=process_model, status="waiting", user=user_one) - process_instance_created_by_user_two_one = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) - _process_instance_created_by_user_two_two = self.create_process_instance_from_process_model(process_model=process_model, status="complete", user=user_two) - process_instance_created_by_user_two_three = self.create_process_instance_from_process_model(process_model=process_model, status="waiting", user=user_two) + process_instance_created_by_user_one_one = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + process_instance_created_by_user_one_three = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="waiting", user=user_one + ) + ) + process_instance_created_by_user_two_one = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="waiting", user=user_two + ) human_task_for_user_one_one = HumanTaskModel( process_instance_id=process_instance_created_by_user_one_one.id, @@ -846,19 +879,252 @@ class TestProcessInstanceReportService(BaseTest): db.session.commit() process_instance_report = ProcessInstanceReportService.report_with_identifier( - user=user_one, report_identifier="system_report_completed_instances_with_tasks_completed_by_me" + user=user_one, + report_identifier="system_report_completed_instances_with_tasks_completed_by_me", ) - report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=process_instance_report, - process_model_identifier=process_model.id, + report_filter = ( + ProcessInstanceReportService.filter_from_metadata_with_overrides( + process_instance_report=process_instance_report, + process_model_identifier=process_model.id, + ) ) response_json = ProcessInstanceReportService.run_process_instance_report( report_filter=report_filter, process_instance_report=process_instance_report, - user=user_one + user=user_one, ) - assert len(response_json['results']) == 1 - assert response_json['results'][0]['process_initiator_id'] == user_two.id - assert response_json['results'][0]['id'] == process_instance_created_by_user_two_one.id - assert response_json['results'][0]['status'] == 'complete' + assert len(response_json["results"]) == 1 + assert response_json["results"][0]["process_initiator_id"] == user_two.id + assert ( + response_json["results"][0]["id"] + == process_instance_created_by_user_two_one.id + ) + assert response_json["results"][0]["status"] == "complete" + + def test_can_filter_by_completed_instances_with_tasks_completed_by_my_groups( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test_can_filter_by_completed_instances_with_tasks_completed_by_my_groups.""" + process_model_id = "runs_without_input/sample" + bpmn_file_location = "sample" + process_model = load_test_spec( + process_model_id, + process_model_source_directory=bpmn_file_location, + ) + user_group_one = GroupModel(identifier="group_one") + user_group_two = GroupModel(identifier="group_two") + db.session.add(user_group_one) + db.session.add(user_group_two) + db.session.commit() + + user_one = self.find_or_create_user(username="user_one") + user_two = self.find_or_create_user(username="user_two") + user_three = self.find_or_create_user(username="user_three") + UserService.add_user_to_group(user_one, user_group_one) + UserService.add_user_to_group(user_two, user_group_one) + UserService.add_user_to_group(user_three, user_group_two) + + # Several processes to ensure they do not return in the result + process_instance_created_by_user_one_one = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + process_instance_created_by_user_one_three = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="waiting", user=user_one + ) + ) + process_instance_created_by_user_two_one = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="waiting", user=user_two + ) + + human_task_for_user_group_one_one = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_one.id, + lane_assignment_id=user_group_one.id, + ) + human_task_for_user_group_one_two = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_three.id, + lane_assignment_id=user_group_one.id, + ) + human_task_for_user_group_one_three = HumanTaskModel( + process_instance_id=process_instance_created_by_user_two_one.id, + lane_assignment_id=user_group_one.id, + ) + human_task_for_user_group_two_one = HumanTaskModel( + process_instance_id=process_instance_created_by_user_two_one.id, + lane_assignment_id=user_group_two.id, + ) + human_task_for_user_group_two_two = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_one.id, + lane_assignment_id=user_group_two.id, + ) + db.session.add(human_task_for_user_group_one_one) + db.session.add(human_task_for_user_group_one_two) + db.session.add(human_task_for_user_group_one_three) + db.session.add(human_task_for_user_group_two_one) + db.session.add(human_task_for_user_group_two_two) + db.session.commit() + + process_instance_report = ProcessInstanceReportService.report_with_identifier( + user=user_one, + report_identifier="system_report_completed_instances_with_tasks_completed_by_my_groups", + ) + report_filter = ( + ProcessInstanceReportService.filter_from_metadata_with_overrides( + process_instance_report=process_instance_report, + process_model_identifier=process_model.id, + ) + ) + response_json = ProcessInstanceReportService.run_process_instance_report( + report_filter=report_filter, + process_instance_report=process_instance_report, + user=user_one, + ) + + assert len(response_json["results"]) == 2 + assert response_json["results"][0]["process_initiator_id"] == user_two.id + assert ( + response_json["results"][0]["id"] + == process_instance_created_by_user_two_one.id + ) + assert response_json["results"][0]["status"] == "complete" + assert response_json["results"][1]["process_initiator_id"] == user_one.id + assert ( + response_json["results"][1]["id"] + == process_instance_created_by_user_one_one.id + ) + assert response_json["results"][1]["status"] == "complete" + + def test_can_filter_by_with_relation_to_me( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test_can_filter_by_with_relation_to_me.""" + process_model_id = "runs_without_input/sample" + bpmn_file_location = "sample" + process_model = load_test_spec( + process_model_id, + process_model_source_directory=bpmn_file_location, + ) + user_group_one = GroupModel(identifier="group_one") + user_group_two = GroupModel(identifier="group_two") + db.session.add(user_group_one) + db.session.add(user_group_two) + db.session.commit() + + user_one = self.find_or_create_user(username="user_one") + user_two = self.find_or_create_user(username="user_two") + user_three = self.find_or_create_user(username="user_three") + UserService.add_user_to_group(user_one, user_group_one) + UserService.add_user_to_group(user_two, user_group_one) + UserService.add_user_to_group(user_three, user_group_two) + + # Several processes to ensure they do not return in the result + process_instance_created_by_user_one_one = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + ) + process_instance_created_by_user_one_two = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_one + ) + ) + process_instance_created_by_user_one_three = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="waiting", user=user_one + ) + ) + process_instance_created_by_user_two_one = ( + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="complete", user=user_two + ) + self.create_process_instance_from_process_model( + process_model=process_model, status="waiting", user=user_two + ) + + human_task_for_user_group_one_one = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_one.id, + lane_assignment_id=user_group_one.id, + ) + human_task_for_user_group_one_two = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_three.id, + lane_assignment_id=user_group_one.id, + ) + human_task_for_user_group_one_three = HumanTaskModel( + process_instance_id=process_instance_created_by_user_two_one.id, + lane_assignment_id=user_group_one.id, + ) + human_task_for_user_group_two_one = HumanTaskModel( + process_instance_id=process_instance_created_by_user_two_one.id, + lane_assignment_id=user_group_two.id, + ) + human_task_for_user_group_two_two = HumanTaskModel( + process_instance_id=process_instance_created_by_user_one_one.id, + lane_assignment_id=user_group_two.id, + ) + db.session.add(human_task_for_user_group_one_one) + db.session.add(human_task_for_user_group_one_two) + db.session.add(human_task_for_user_group_one_three) + db.session.add(human_task_for_user_group_two_one) + db.session.add(human_task_for_user_group_two_two) + db.session.commit() + + UserService.add_user_to_human_tasks_if_appropriate(user_one) + + process_instance_report = ProcessInstanceReportService.report_with_identifier( + user=user_one + ) + report_filter = ( + ProcessInstanceReportService.filter_from_metadata_with_overrides( + process_instance_report=process_instance_report, + process_model_identifier=process_model.id, + with_relation_to_me=True, + ) + ) + response_json = ProcessInstanceReportService.run_process_instance_report( + report_filter=report_filter, + process_instance_report=process_instance_report, + user=user_one, + ) + + assert len(response_json["results"]) == 4 + process_instance_ids_in_results = [r["id"] for r in response_json["results"]] + assert ( + process_instance_created_by_user_one_one.id + in process_instance_ids_in_results + ) + assert ( + process_instance_created_by_user_one_two.id + in process_instance_ids_in_results + ) + assert ( + process_instance_created_by_user_one_three.id + in process_instance_ids_in_results + ) + assert ( + process_instance_created_by_user_two_one.id + in process_instance_ids_in_results + ) From 51c3cbec851a293e676911eebfc62b93b40b656c Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 14:58:15 -0500 Subject: [PATCH 6/9] set the completed by user on human task w/ burnettk --- .../services/process_instance_processor.py | 12 ++++++++---- .../services/process_instance_service.py | 2 +- .../unit/test_process_instance_processor.py | 5 ++++- 3 files changed, 13 insertions(+), 6 deletions(-) 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 753e3cd0..24dbd497 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -573,10 +573,9 @@ class ProcessInstanceProcessor: ) return details_model - def save_spiff_step_details(self, human_task: HumanTaskModel) -> None: + def save_spiff_step_details(self) -> None: """SaveSpiffStepDetails.""" details_model = self.spiff_step_details() - # details_model.lane_assignment_id = human_task.lane_assignment_id db.session.add(details_model) db.session.commit() @@ -1180,11 +1179,16 @@ class ProcessInstanceProcessor: ) return user_tasks # type: ignore - def complete_task(self, task: SpiffTask, human_task: HumanTaskModel) -> None: + def complete_task( + self, task: SpiffTask, human_task: HumanTaskModel, user: UserModel + ) -> None: """Complete_task.""" self.increment_spiff_step() self.bpmn_process_instance.complete_task_from_id(task.id) - self.save_spiff_step_details(human_task) + human_task.completed_by_user_id = user.id + db.session.add(human_task) + db.session.commit() + self.save_spiff_step_details() def get_data(self) -> dict[str, Any]: """Get_data.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index 7eee445a..e933eda9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -210,7 +210,7 @@ class ProcessInstanceService: dot_dct = ProcessInstanceService.create_dot_dict(data) spiff_task.update_data(dot_dct) # ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store. - processor.complete_task(spiff_task, human_task) + processor.complete_task(spiff_task, human_task, user=user) processor.do_engine_steps(save=True) @staticmethod diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index 100651ef..1a96ca88 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -179,6 +179,7 @@ class TestProcessInstanceProcessor(BaseTest): ProcessInstanceService.complete_form_task( processor, spiff_task, {}, initiator_user, human_task ) + assert human_task.completed_by_user_id == initiator_user.id assert len(process_instance.human_tasks) == 1 human_task = process_instance.human_tasks[0] @@ -198,6 +199,7 @@ class TestProcessInstanceProcessor(BaseTest): ProcessInstanceService.complete_form_task( processor, spiff_task, {}, finance_user_three, human_task ) + assert human_task.completed_by_user_id == finance_user_three.id assert len(process_instance.human_tasks) == 1 human_task = process_instance.human_tasks[0] assert human_task.lane_assignment_id is None @@ -215,6 +217,7 @@ class TestProcessInstanceProcessor(BaseTest): ProcessInstanceService.complete_form_task( processor, spiff_task, {}, finance_user_four, human_task ) + assert human_task.completed_by_user_id == finance_user_four.id assert len(process_instance.human_tasks) == 1 human_task = process_instance.human_tasks[0] assert human_task.lane_assignment_id is None @@ -250,7 +253,7 @@ class TestProcessInstanceProcessor(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_sets_permission_correctly_on_human_task_when_using_dict.""" + """Test_does_not_recreate_human_tasks_on_multiple_saves.""" self.create_process_group( client, with_super_admin_user, "test_group", "test_group" ) From 1d3f1f7468343ab6d1d9a70f797243f20fee95f2 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 15:02:27 -0500 Subject: [PATCH 7/9] a little refactor w/ burnettk --- .../routes/process_api_blueprint.py | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 094b14bb..5c0fdb5c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -172,7 +172,7 @@ def process_group_add(body: dict) -> flask.wrappers.Response: """Add_process_group.""" process_group = ProcessGroup(**body) ProcessModelService.add_process_group(process_group) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} added process group {process_group.id}" ) return make_response(jsonify(process_group), 201) @@ -182,7 +182,7 @@ def process_group_delete(modified_process_group_id: str) -> flask.wrappers.Respo """Process_group_delete.""" process_group_id = un_modify_modified_process_model_id(modified_process_group_id) ProcessModelService().process_group_delete(process_group_id) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} deleted process group {process_group_id}" ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -202,7 +202,7 @@ def process_group_update( process_group_id = un_modify_modified_process_model_id(modified_process_group_id) process_group = ProcessGroup(id=process_group_id, **body_filtered) ProcessModelService.update_process_group(process_group) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} updated process group {process_group_id}" ) return make_response(jsonify(process_group), 200) @@ -269,7 +269,7 @@ def process_group_move( new_process_group = ProcessModelService().process_group_move( original_process_group_id, new_location ) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} moved process group {original_process_group_id} to {new_process_group.id}" ) return make_response(jsonify(new_process_group), 200) @@ -320,7 +320,7 @@ def process_model_create( ) ProcessModelService.add_process_model(process_model_info) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} created process model {process_model_info.id}" ) return Response( @@ -336,7 +336,7 @@ def process_model_delete( """Process_model_delete.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") ProcessModelService().process_model_delete(process_model_identifier) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} deleted process model {process_model_identifier}" ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -362,7 +362,7 @@ def process_model_update( process_model = get_process_model(process_model_identifier) ProcessModelService.update_process_model(process_model, body_filtered) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} updated process model {process_model_identifier}" ) return ProcessModelInfoSchema().dump(process_model) @@ -396,7 +396,7 @@ def process_model_move( new_process_model = ProcessModelService().process_model_move( original_process_model_id, new_location ) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} moved process model {original_process_model_id} to {new_process_model.id}" ) return make_response(jsonify(new_process_model), 200) @@ -495,7 +495,7 @@ def process_model_file_update( ) SpecFileService.update_file(process_model, file_name, request_file_contents) - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} clicked save for {process_model_identifier}/{file_name}" ) @@ -519,7 +519,7 @@ def process_model_file_delete( ) ) from exception - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} deleted process model file {process_model_identifier}/{file_name}" ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -543,7 +543,7 @@ def add_file(modified_process_model_identifier: str) -> flask.wrappers.Response: file_contents = SpecFileService.get_data(process_model, file.name) file.file_contents = file_contents file.process_model_id = process_model.id - commit_and_push_to_git( + _commit_and_push_to_git( f"User: {g.user.username} added process model file {process_model_identifier}/{file.name}" ) return Response( @@ -1930,6 +1930,57 @@ def delete_secret(key: str) -> Response: return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") +def update_task_data( + process_instance_id: str, + modified_process_model_identifier: str, + task_id: str, + body: Dict, +) -> Response: + """Update task data.""" + process_instance = ProcessInstanceModel.query.filter( + ProcessInstanceModel.id == int(process_instance_id) + ).first() + if process_instance: + if process_instance.status != "suspended": + raise ProcessInstanceTaskDataCannotBeUpdatedError( + f"The process instance needs to be suspended to udpate the task-data. It is currently: {process_instance.status}" + ) + + process_instance_bpmn_json_dict = json.loads(process_instance.bpmn_json) + if "new_task_data" in body: + new_task_data_str: str = body["new_task_data"] + new_task_data_dict = json.loads(new_task_data_str) + if task_id in process_instance_bpmn_json_dict["tasks"]: + process_instance_bpmn_json_dict["tasks"][task_id][ + "data" + ] = new_task_data_dict + process_instance.bpmn_json = json.dumps(process_instance_bpmn_json_dict) + db.session.add(process_instance) + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise ApiError( + error_code="update_task_data_error", + message=f"Could not update the Instance. Original error is {e}", + ) from e + else: + raise ApiError( + error_code="update_task_data_error", + message=f"Could not find Task: {task_id} in Instance: {process_instance_id}.", + ) + else: + raise ApiError( + error_code="update_task_data_error", + message=f"Could not update task data for Instance: {process_instance_id}, and Task: {task_id}.", + ) + return Response( + json.dumps(ProcessInstanceModelSchema().dump(process_instance)), + status=200, + mimetype="application/json", + ) + + def _get_required_parameter_or_raise(parameter: str, post_body: dict[str, Any]) -> Any: """Get_required_parameter_or_raise.""" return_value = None @@ -2006,58 +2057,7 @@ def _update_form_schema_with_task_data_as_needed( _update_form_schema_with_task_data_as_needed(o, task_data) -def update_task_data( - process_instance_id: str, - modified_process_model_identifier: str, - task_id: str, - body: Dict, -) -> Response: - """Update task data.""" - process_instance = ProcessInstanceModel.query.filter( - ProcessInstanceModel.id == int(process_instance_id) - ).first() - if process_instance: - if process_instance.status != "suspended": - raise ProcessInstanceTaskDataCannotBeUpdatedError( - f"The process instance needs to be suspended to udpate the task-data. It is currently: {process_instance.status}" - ) - - process_instance_bpmn_json_dict = json.loads(process_instance.bpmn_json) - if "new_task_data" in body: - new_task_data_str: str = body["new_task_data"] - new_task_data_dict = json.loads(new_task_data_str) - if task_id in process_instance_bpmn_json_dict["tasks"]: - process_instance_bpmn_json_dict["tasks"][task_id][ - "data" - ] = new_task_data_dict - process_instance.bpmn_json = json.dumps(process_instance_bpmn_json_dict) - db.session.add(process_instance) - try: - db.session.commit() - except Exception as e: - db.session.rollback() - raise ApiError( - error_code="update_task_data_error", - message=f"Could not update the Instance. Original error is {e}", - ) from e - else: - raise ApiError( - error_code="update_task_data_error", - message=f"Could not find Task: {task_id} in Instance: {process_instance_id}.", - ) - else: - raise ApiError( - error_code="update_task_data_error", - message=f"Could not update task data for Instance: {process_instance_id}, and Task: {task_id}.", - ) - return Response( - json.dumps(ProcessInstanceModelSchema().dump(process_instance)), - status=200, - mimetype="application/json", - ) - - -def commit_and_push_to_git(message: str) -> None: +def _commit_and_push_to_git(message: str) -> None: """Commit_and_push_to_git.""" if current_app.config["GIT_COMMIT_ON_SAVE"]: git_output = GitService.commit(message=message) From 87a1eba1e060a94ef2da050b92384829d05ded36 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 15:41:01 -0500 Subject: [PATCH 8/9] fixed completed instances filter w/ burnettk --- .../services/process_instance_report_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index 773533ae..d04b81f9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -380,7 +380,11 @@ class ProcessInstanceReportService: ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore ) - if report_filter.with_relation_to_me is True: + if ( + not report_filter.with_tasks_completed_by_me + and not report_filter.with_tasks_assigned_to_my_group + and report_filter.with_relation_to_me is True + ): process_instance_query = process_instance_query.outerjoin( HumanTaskModel ).outerjoin( From d7ea9ebfe8b37c4502ce9f2e562fbe063d6afcb8 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 20 Dec 2022 16:02:27 -0500 Subject: [PATCH 9/9] fixed get tasks and process instances by group w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 6 +++--- .../services/process_instance_report_service.py | 17 +++++++---------- .../src/components/TasksWaitingForMyGroups.tsx | 2 +- .../src/routes/CompletedInstances.tsx | 2 +- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 0d3a4afe..12b40160 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -601,7 +601,7 @@ paths: description: Specifies the identifier of a report to use, if any schema: type: integer - - name: group_identifier + - name: user_group_identifier in: query required: false description: The identifier of the group to get the process instances for @@ -714,7 +714,7 @@ paths: description: Specifies the identifier of a report to use, if any schema: type: integer - - name: group_identifier + - name: user_group_identifier in: query required: false description: The identifier of the group to get the process instances for @@ -1328,7 +1328,7 @@ paths: /tasks/for-my-groups: parameters: - - name: group_identifier + - name: user_group_identifier in: query required: false description: The identifier of the group to get the tasks for diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index d04b81f9..82a35fc5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -414,17 +414,14 @@ class ProcessInstanceReportService: ) if report_filter.with_tasks_assigned_to_my_group is True: + group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] if report_filter.user_group_identifier: - process_instance_query = process_instance_query.join( - GroupModel, - GroupModel.identifier == report_filter.user_group_identifier, - ) - else: - process_instance_query = process_instance_query.join(HumanTaskModel) - process_instance_query = process_instance_query.join( - GroupModel, - GroupModel.id == HumanTaskModel.lane_assignment_id, - ) + group_model_join_conditions.append(GroupModel.identifier == report_filter.user_group_identifier) + process_instance_query = process_instance_query.join(HumanTaskModel) + process_instance_query = process_instance_query.join( + GroupModel, + and_(*group_model_join_conditions) + ) process_instance_query = process_instance_query.join( UserGroupAssignmentModel, UserGroupAssignmentModel.group_id == GroupModel.id, diff --git a/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx b/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx index b7515c91..dab0372b 100644 --- a/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx +++ b/spiffworkflow-frontend/src/components/TasksWaitingForMyGroups.tsx @@ -21,7 +21,7 @@ export default function TasksWaitingForMyGroups() { return ( );