From ac6706197c9b8f5bf826e0e639c8f3f9d77de8ad Mon Sep 17 00:00:00 2001 From: burnettk Date: Sun, 13 Nov 2022 18:42:04 -0500 Subject: [PATCH] Squashed 'spiffworkflow-backend/' changes from 2cb3fb27e2..55f5c8113e 55f5c8113e arena github actions a06427482b reduce matrix f5f88926a6 debug 23fbe1b2e9 lint and mypy 30ee07700f Merge remote-tracking branch 'origin/main' into feature/home_page_redesign bd38d90600 lint 201655fc0f add the username to the task list w/ burnettk e069906394 merged in main and resolved conflicts w/ burnettk 9135f74a03 added more task tables w/ burnettk 352353e48b store bpmn_file_relative_path using correct slashes w/ burnettk d02a6610bb underscore unused vars a9ecaa0431 added tasks for my open processes page w/ burnettk 74af6a4ad1 Merge remote-tracking branch 'origin/main' into feature/home_page_redesign d5330d6031 Merge remote-tracking branch 'origin/main' into feature/home_page_redesign 37d9ca8dea added home page routes and some tab stuff w/ burnettk git-subtree-dir: spiffworkflow-backend git-subtree-split: 55f5c8113e1189888672992d109e80a3d51dfa1a --- .github/workflows/tests.yml | 2 +- src/spiffworkflow_backend/__init__.py | 7 +- src/spiffworkflow_backend/api.yml | 58 ++++++++++++++ .../models/process_instance.py | 4 +- .../models/process_model.py | 6 ++ .../routes/process_api_blueprint.py | 76 ++++++++++++++++++- .../services/process_model_service.py | 2 +- .../services/spec_file_service.py | 2 +- .../integration/test_process_api.py | 23 ++++-- 9 files changed, 164 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31bb2c7cf..f1db9198c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -156,7 +156,7 @@ jobs: - name: Upload coverage data # pin to upload coverage from only one matrix entry, otherwise coverage gets confused later - if: always() && matrix.session == 'tests' && matrix.python == '3.11' && matrix.os == 'ubuntu-latest' + if: always() && matrix.session == 'tests' && matrix.python == '3.11' && matrix.os == 'ubuntu-latest' && matrix.database == 'mysql' uses: "actions/upload-artifact@v3.0.0" with: name: coverage-data diff --git a/src/spiffworkflow_backend/__init__.py b/src/spiffworkflow_backend/__init__.py index 6ca51aef2..d17beac3c 100644 --- a/src/spiffworkflow_backend/__init__.py +++ b/src/spiffworkflow_backend/__init__.py @@ -39,11 +39,14 @@ class MyJSONEncoder(DefaultJSONProvider): return_dict = {} for row_key in obj.keys(): row_value = obj[row_key] - if hasattr(row_value, "__dict__"): + if hasattr(row_value, "serialized"): + return_dict.update(row_value.serialized) + elif hasattr(row_value, "__dict__"): return_dict.update(row_value.__dict__) else: return_dict.update({row_key: row_value}) - return_dict.pop("_sa_instance_state") + if "_sa_instance_state" in return_dict: + return_dict.pop("_sa_instance_state") return return_dict return super().default(obj) diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index 14a0e52f6..a87d5a2ff 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -872,6 +872,64 @@ paths: items: $ref: "#/components/schemas/Task" + /tasks/for-my-open-processes: + parameters: + - name: page + in: query + required: false + description: The page number to return. Defaults to page 1. + schema: + type: integer + - name: per_page + in: query + required: false + description: The page number to return. Defaults to page 1. + schema: + type: integer + get: + tags: + - Process Instances + operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_my_open_processes + summary: returns the list of tasks for given user's open process instances + responses: + "200": + description: list of tasks + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Task" + + /tasks/for-processes-started-by-others: + parameters: + - name: page + in: query + required: false + description: The page number to return. Defaults to page 1. + schema: + type: integer + - name: per_page + in: query + required: false + description: The page number to return. Defaults to page 1. + schema: + type: integer + get: + tags: + - Process Instances + operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_processes_started_by_others + summary: returns the list of tasks for given user's open process instances + responses: + "200": + description: list of tasks + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Task" + /process-instance/{process_instance_id}/tasks: parameters: - name: process_instance_id diff --git a/src/spiffworkflow_backend/models/process_instance.py b/src/spiffworkflow_backend/models/process_instance.py index cfa892528..2e94e9949 100644 --- a/src/spiffworkflow_backend/models/process_instance.py +++ b/src/spiffworkflow_backend/models/process_instance.py @@ -100,17 +100,17 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): local_bpmn_xml_file_contents = "" if self.bpmn_xml_file_contents: local_bpmn_xml_file_contents = self.bpmn_xml_file_contents.decode("utf-8") - return { "id": self.id, "process_model_identifier": self.process_model_identifier, "process_group_identifier": self.process_group_identifier, "status": self.status, - "bpmn_json": self.bpmn_json, "start_in_seconds": self.start_in_seconds, "end_in_seconds": self.end_in_seconds, "process_initiator_id": self.process_initiator_id, "bpmn_xml_file_contents": local_bpmn_xml_file_contents, + "bpmn_version_control_identifier": self.bpmn_version_control_identifier, + "bpmn_version_control_type": self.bpmn_version_control_type, "spiff_step": self.spiff_step, } diff --git a/src/spiffworkflow_backend/models/process_model.py b/src/spiffworkflow_backend/models/process_model.py index d63197be2..3ab55d07c 100644 --- a/src/spiffworkflow_backend/models/process_model.py +++ b/src/spiffworkflow_backend/models/process_model.py @@ -2,6 +2,7 @@ from __future__ import annotations import enum +import os from dataclasses import dataclass from dataclasses import field from typing import Any @@ -50,6 +51,11 @@ class ProcessModelInfo: return True return False + # for use with os.path.join so it can work on windows + def id_for_file_path(self) -> str: + """Id_for_file_path.""" + return self.id.replace("/", os.sep) + class ProcessModelInfoSchema(Schema): """ProcessModelInfoSchema.""" diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index 17b64d4cc..0042adcfc 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -28,6 +28,7 @@ from lxml import etree # type: ignore from lxml.builder import ElementMaker # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState +from sqlalchemy import and_ from sqlalchemy import asc from sqlalchemy import desc @@ -37,6 +38,7 @@ from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( from spiffworkflow_backend.models.active_task import ActiveTaskModel from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel from spiffworkflow_backend.models.file import FileSchema +from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_model import MessageModel from spiffworkflow_backend.models.message_triggerable_process_model import ( @@ -1000,6 +1002,67 @@ def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Res return make_response(jsonify(response_json), 200) +def task_list_for_my_open_processes( + page: int = 1, per_page: int = 100 +) -> flask.wrappers.Response: + """Task_list_for_my_open_processes.""" + return get_tasks(page=page, per_page=per_page) + + +def task_list_for_processes_started_by_others( + page: int = 1, per_page: int = 100 +) -> flask.wrappers.Response: + """Task_list_for_processes_started_by_others.""" + return get_tasks(processes_started_by_user=False, page=page, per_page=per_page) + + +def get_tasks( + processes_started_by_user: bool = True, page: int = 1, per_page: int = 100 +) -> flask.wrappers.Response: + """Get_tasks.""" + user_id = g.user.id + active_tasks_query = ( + ActiveTaskModel.query.outerjoin( + GroupModel, GroupModel.id == ActiveTaskModel.lane_assignment_id + ) + .join(ProcessInstanceModel) + .join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) + ) + + if processes_started_by_user: + active_tasks_query = active_tasks_query.filter( + ProcessInstanceModel.process_initiator_id == user_id + ).outerjoin(ActiveTaskUserModel, and_(ActiveTaskUserModel.user_id == user_id)) + else: + active_tasks_query = active_tasks_query.filter( + ProcessInstanceModel.process_initiator_id != user_id + ).join(ActiveTaskUserModel, and_(ActiveTaskUserModel.user_id == user_id)) + + active_tasks = active_tasks_query.add_columns( + ProcessInstanceModel.process_model_identifier, + ProcessInstanceModel.status.label("process_instance_status"), # type: ignore + ProcessInstanceModel.updated_at_in_seconds, + ProcessInstanceModel.created_at_in_seconds, + UserModel.username, + GroupModel.identifier.label("group_identifier"), + ActiveTaskModel.task_name, + ActiveTaskModel.task_title, + ActiveTaskModel.process_model_display_name, + ActiveTaskModel.process_instance_id, + ActiveTaskUserModel.user_id.label("current_user_is_potential_owner"), + ).paginate(page=page, per_page=per_page, error_out=False) + + response_json = { + "results": active_tasks.items, + "pagination": { + "count": len(active_tasks.items), + "total": active_tasks.total, + "pages": active_tasks.pages, + }, + } + return make_response(jsonify(response_json), 200) + + def process_instance_task_list( process_instance_id: int, all_tasks: bool = False, spiff_step: int = 0 ) -> flask.wrappers.Response: @@ -1354,9 +1417,18 @@ def find_process_instance_by_id_or_raise( process_instance_id: int, ) -> ProcessInstanceModel: """Find_process_instance_by_id_or_raise.""" - process_instance = ProcessInstanceModel.query.filter_by( + process_instance_query = ProcessInstanceModel.query.filter_by( id=process_instance_id - ).first() + ) + + # we had a frustrating session trying to do joins and access columns from two tables. here's some notes for our future selves: + # this returns an object that allows you to do: process_instance.UserModel.username + # process_instance = db.session.query(ProcessInstanceModel, UserModel).filter_by(id=process_instance_id).first() + # you can also use splat with add_columns, but it still didn't ultimately give us access to the process instance + # attributes or username like we wanted: + # process_instance_query.join(UserModel).add_columns(*ProcessInstanceModel.__table__.columns, UserModel.username) + + process_instance = process_instance_query.first() if process_instance is None: raise ( ApiError( diff --git a/src/spiffworkflow_backend/services/process_model_service.py b/src/spiffworkflow_backend/services/process_model_service.py index f473a4ca0..06d0a7d7a 100644 --- a/src/spiffworkflow_backend/services/process_model_service.py +++ b/src/spiffworkflow_backend/services/process_model_service.py @@ -218,7 +218,7 @@ class ProcessModelService(FileSystemService): def __get_all_nested_models(self, group_path: str) -> list: """__get_all_nested_models.""" all_nested_models = [] - for root, dirs, files in os.walk(group_path): + for _root, dirs, _files in os.walk(group_path): for dir in dirs: model_dir = os.path.join(group_path, dir) if ProcessModelService().is_model(model_dir): diff --git a/src/spiffworkflow_backend/services/spec_file_service.py b/src/spiffworkflow_backend/services/spec_file_service.py index 92e905bf3..149461472 100644 --- a/src/spiffworkflow_backend/services/spec_file_service.py +++ b/src/spiffworkflow_backend/services/spec_file_service.py @@ -375,7 +375,7 @@ class SpecFileService(FileSystemService): process_model_info: ProcessModelInfo, bpmn_file_name: str, et_root: _Element ) -> None: """Store_bpmn_process_identifiers.""" - relative_process_model_path = process_model_info.id + relative_process_model_path = process_model_info.id_for_file_path() relative_bpmn_file_path = os.path.join( relative_process_model_path, bpmn_file_name diff --git a/tests/spiffworkflow_backend/integration/test_process_api.py b/tests/spiffworkflow_backend/integration/test_process_api.py index a45336841..7623537d2 100644 --- a/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1441,7 +1441,7 @@ class TestProcessApi(BaseTest): updated_at_in_seconds=round(time.time()), start_in_seconds=(1000 * i) + 1000, end_in_seconds=(1000 * i) + 2000, - bpmn_json=json.dumps({"i": i}), + bpmn_version_control_identifier=i, ) db.session.add(process_instance) db.session.commit() @@ -1487,7 +1487,12 @@ class TestProcessApi(BaseTest): results = response.json["results"] assert len(results) == 4 for i in range(4): - assert json.loads(results[i]["bpmn_json"])["i"] in (1, 2, 3, 4) + assert json.loads(results[i]["bpmn_version_control_identifier"]) in ( + 1, + 2, + 3, + 4, + ) # start > 2000, end < 5000 - this should eliminate the first 2 and the last response = client.get( @@ -1497,8 +1502,8 @@ class TestProcessApi(BaseTest): assert response.json is not None results = response.json["results"] assert len(results) == 2 - assert json.loads(results[0]["bpmn_json"])["i"] in (2, 3) - assert json.loads(results[1]["bpmn_json"])["i"] in (2, 3) + assert json.loads(results[0]["bpmn_version_control_identifier"]) in (2, 3) + assert json.loads(results[1]["bpmn_version_control_identifier"]) in (2, 3) # start > 1000, start < 4000 - this should eliminate the first and the last 2 response = client.get( @@ -1508,8 +1513,8 @@ class TestProcessApi(BaseTest): assert response.json is not None results = response.json["results"] assert len(results) == 2 - assert json.loads(results[0]["bpmn_json"])["i"] in (1, 2) - assert json.loads(results[1]["bpmn_json"])["i"] in (1, 2) + assert json.loads(results[0]["bpmn_version_control_identifier"]) in (1, 2) + assert json.loads(results[1]["bpmn_version_control_identifier"]) in (1, 2) # end > 2000, end < 6000 - this should eliminate the first and the last response = client.get( @@ -1520,7 +1525,11 @@ class TestProcessApi(BaseTest): results = response.json["results"] assert len(results) == 3 for i in range(3): - assert json.loads(results[i]["bpmn_json"])["i"] in (1, 2, 3) + assert json.loads(results[i]["bpmn_version_control_identifier"]) in ( + 1, + 2, + 3, + ) def test_process_instance_report_list( self,