diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py index 9f1a74e70..a6dc7f38e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py @@ -38,11 +38,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/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 7f82d29b8..a87d5a2ff 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -901,6 +901,35 @@ paths: 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/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index cc53623ee..ee95007b1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -99,17 +99,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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index a5b92870e..592d6d5fb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -76,7 +76,7 @@ from spiffworkflow_backend.services.secret_service import SecretService from spiffworkflow_backend.services.service_task_service import ServiceTaskService from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService -from sqlalchemy import asc +from sqlalchemy import and_, asc from sqlalchemy import desc @@ -996,14 +996,30 @@ def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Res return make_response(jsonify(response_json), 200) -# @process_api_blueprint.route("/v1.0/tasks", methods=["GET"]) def task_list_for_my_open_processes(page: int = 1, per_page: int = 100) -> flask.wrappers.Response: + 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: + 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: user_id = g.user.id - active_tasks = ( - ActiveTaskModel.query.order_by(desc(ActiveTaskModel.id)) # type: ignore + active_tasks_query = ( + ActiveTaskModel.query .join(ProcessInstanceModel) - .filter_by(process_initiator_id=user_id) - .outerjoin(GroupModel) + .order_by(desc(ProcessInstanceModel.created_at_in_seconds)) # type: ignore + ) + + if processes_started_by_user: + active_tasks_query = active_tasks_query.filter_by(process_initiator_id=user_id) + else: + active_tasks_query = active_tasks_query.filter(ProcessInstanceModel.process_initiator_id != user_id) + + active_tasks = ( + active_tasks_query.outerjoin(GroupModel) + .outerjoin(ActiveTaskUserModel, and_(ActiveTaskUserModel.user_id == user_id)) # just need this add_columns to add the process_model_identifier. Then add everything back that was removed. .add_columns( ProcessInstanceModel.process_model_identifier, @@ -1015,10 +1031,10 @@ def task_list_for_my_open_processes(page: int = 1, per_page: int = 100) -> flask 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) ) - # tasks = [ActiveTaskModel.to_task(active_task) for active_task in active_tasks.items] response_json = { "results": active_tasks.items, @@ -1028,7 +1044,6 @@ def task_list_for_my_open_processes(page: int = 1, per_page: int = 100) -> flask "pages": active_tasks.pages, }, } - return make_response(jsonify(response_json), 200) @@ -1383,12 +1398,21 @@ def find_principal_or_raise() -> PrincipalModel: def find_process_instance_by_id_or_raise( - process_instance_id: int, + 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/spiffworkflow-frontend/src/components/MyTasksForProcessesStartedByOthers.tsx b/spiffworkflow-frontend/src/components/MyTasksForProcessesStartedByOthers.tsx new file mode 100644 index 000000000..d8de933d0 --- /dev/null +++ b/spiffworkflow-frontend/src/components/MyTasksForProcessesStartedByOthers.tsx @@ -0,0 +1,137 @@ +import { useEffect, useState } from 'react'; +// @ts-ignore +import { Button, Table } from '@carbon/react'; +import { Link, useSearchParams } from 'react-router-dom'; +import PaginationForTable from './PaginationForTable'; +import { + convertSecondsToFormattedDateTime, + getPageInfoFromSearchParams, + modifyProcessModelPath, +} from '../helpers'; +import HttpService from '../services/HttpService'; +import { PaginationObject } from '../interfaces'; + +const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; + +export default function MyTasksForProcessesStartedByOthers() { + const [searchParams] = useSearchParams(); + const [tasks, setTasks] = useState([]); + const [pagination, setPagination] = useState(null); + + useEffect(() => { + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE + ); + const setTasksFromResult = (result: any) => { + setTasks(result.results); + setPagination(result.pagination); + }; + HttpService.makeCallToBackend({ + path: `/tasks/for-processes-started-by-others?per_page=${perPage}&page=${page}`, + successCallback: setTasksFromResult, + }); + }, [searchParams]); + + const buildTable = () => { + const rows = tasks.map((row) => { + const rowToUse = row as any; + const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; + const modifiedProcessModelIdentifier = modifyProcessModelPath( + rowToUse.process_model_identifier + ); + return ( + + + + {rowToUse.process_model_display_name} + + + + + View {rowToUse.process_instance_id} + + + + {rowToUse.task_title} + + {rowToUse.process_instance_status} + {rowToUse.group_identifier || '-'} + + {convertSecondsToFormattedDateTime( + rowToUse.created_at_in_seconds + ) || '-'} + + + {convertSecondsToFormattedDateTime( + rowToUse.updated_at_in_seconds + ) || '-'} + + + + + + ); + }); + return ( + + + + + + + + + + + + + + {rows} +
Process ModelProcess InstanceTask NameProcess Instance StatusAssigned GroupProcess StartedProcess UpdatedActions
+ ); + }; + + const tasksComponent = () => { + if (pagination && pagination.total < 1) { + return null; + } + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE + ); + return ( + <> +

Tasks waiting for me

+ + + ); + }; + + if (pagination) { + return tasksComponent(); + } + return null; +} diff --git a/spiffworkflow-frontend/src/routes/TasksForMyOpenProcesses.tsx b/spiffworkflow-frontend/src/components/TasksForMyOpenProcesses.tsx similarity index 92% rename from spiffworkflow-frontend/src/routes/TasksForMyOpenProcesses.tsx rename to spiffworkflow-frontend/src/components/TasksForMyOpenProcesses.tsx index 9ac843345..8a2f0bddb 100644 --- a/spiffworkflow-frontend/src/routes/TasksForMyOpenProcesses.tsx +++ b/spiffworkflow-frontend/src/components/TasksForMyOpenProcesses.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; // @ts-ignore import { Button, Table } from '@carbon/react'; import { Link, useSearchParams } from 'react-router-dom'; -import PaginationForTable from '../components/PaginationForTable'; +import PaginationForTable from './PaginationForTable'; import { convertSecondsToFormattedDateTime, getPageInfoFromSearchParams, @@ -36,7 +36,7 @@ export default function MyOpenProcesses() { const buildTable = () => { const rows = tasks.map((row) => { const rowToUse = row as any; - const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.id}`; + const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; const modifiedProcessModelIdentifier = modifyProcessModelPath( rowToUse.process_model_identifier ); @@ -80,6 +80,7 @@ export default function MyOpenProcesses() { variant="primary" href={taskUrl} hidden={rowToUse.process_instance_status === 'suspended'} + disabled={!rowToUse.current_user_is_potential_owner} > Go @@ -106,7 +107,7 @@ export default function MyOpenProcesses() { ); }; - const tasksWaitingForMeComponent = () => { + const tasksComponent = () => { if (pagination && pagination.total < 1) { return null; } @@ -129,13 +130,8 @@ export default function MyOpenProcesses() { ); }; - const tasksWaitingForMe = tasksWaitingForMeComponent(); - if (pagination) { - if (tasksWaitingForMe === null) { - return

No tasks are waiting for you.

; - } - return <>{tasksWaitingForMe}; + return tasksComponent(); } return null; } diff --git a/spiffworkflow-frontend/src/routes/GroupedTasks.tsx b/spiffworkflow-frontend/src/routes/GroupedTasks.tsx new file mode 100644 index 000000000..7312bc554 --- /dev/null +++ b/spiffworkflow-frontend/src/routes/GroupedTasks.tsx @@ -0,0 +1,12 @@ +import MyTasksForProcessesStartedByOthers from '../components/MyTasksForProcessesStartedByOthers'; +import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses'; + +export default function GroupedTasks() { + return ( + <> + +
+ + + ); +} diff --git a/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx b/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx index cbd1bd4e2..d02cb9d72 100644 --- a/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx +++ b/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx @@ -5,7 +5,7 @@ import { Tabs, TabList, Tab } from '@carbon/react'; import TaskShow from './TaskShow'; import ErrorContext from '../contexts/ErrorContext'; import MyTasks from './MyTasks'; -import TasksForMyOpenProcesses from './TasksForMyOpenProcesses'; +import GroupedTasks from './GroupedTasks'; export default function HomePageRoutes() { const location = useLocation(); @@ -16,7 +16,7 @@ export default function HomePageRoutes() { useEffect(() => { setErrorMessage(null); let newSelectedTabIndex = 0; - if (location.pathname.match(/^\/tasks\/for-my-open-processes/)) { + if (location.pathname.match(/^\/tasks\/grouped\b/)) { newSelectedTabIndex = 1; } setSelectedTabIndex(newSelectedTabIndex); @@ -27,9 +27,7 @@ export default function HomePageRoutes() { navigate('/tasks/my-tasks')}>My Tasks - navigate('/tasks/for-my-open-processes')}> - Tasks for My Open Processes - + navigate('/tasks/grouped')}>Grouped Tasks
@@ -37,10 +35,7 @@ export default function HomePageRoutes() { } /> } /> } /> - } - /> + } /> );