added ability to filter process instances by process initiator

This commit is contained in:
jasquat 2023-01-04 16:11:52 -05:00
parent 58724ef408
commit 98c3a9a511
8 changed files with 168 additions and 19 deletions

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
function error_handler() {
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
exit "$2"
}
trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail
grep -E '^ +\/' src/spiffworkflow_backend/api.yml | sort

View File

@ -1414,11 +1414,34 @@ paths:
items:
$ref: "#/components/schemas/Task"
/users/search:
parameters:
- name: username_prefix
in: query
required: true
description: The prefix of the user
schema:
type: string
get:
tags:
- Users
operationId: spiffworkflow_backend.routes.users_controller.user_search
summary: Returns a list of users that the search param
responses:
"200":
description: list of users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
/user-groups/for-current-user:
get:
tags:
- Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.user_group_list_for_current_user
- User Groups
operationId: spiffworkflow_backend.routes.users_controller.user_group_list_for_current_user
summary: Group identifiers for current logged in user
responses:
"200":

View File

@ -1,6 +1,8 @@
"""User."""
from __future__ import annotations
from dataclasses import dataclass
import jwt
import marshmallow
from flask import current_app
@ -16,15 +18,18 @@ class UserNotFoundError(Exception):
"""UserNotFoundError."""
@dataclass
class UserModel(SpiffworkflowBaseDBModel):
"""UserModel."""
__tablename__ = "user"
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
id = db.Column(db.Integer, primary_key=True)
username = db.Column(
id: int = db.Column(db.Integer, primary_key=True)
username: str = db.Column(
db.String(255), nullable=False, unique=True
) # should always be a unique value
)
service = db.Column(
db.String(255), nullable=False, unique=False
) # not 'openid' -- google, aws

View File

@ -71,14 +71,6 @@ def permissions_check(body: Dict[str, Dict[str, list[str]]]) -> flask.wrappers.R
return make_response(jsonify({"results": response_dict}), 200)
def user_group_list_for_current_user() -> flask.wrappers.Response:
"""User_group_list_for_current_user."""
groups = g.user.groups
# TODO: filter out the default group and have a way to know what is the default group
group_identifiers = [i.identifier for i in groups if i.identifier != "everybody"]
return make_response(jsonify(sorted(group_identifiers)), 200)
def process_list() -> Any:
"""Returns a list of all known processes.

View File

@ -0,0 +1,26 @@
"""users_controller."""
import flask
from spiffworkflow_backend.models.user import UserModel
from flask import make_response
from flask import jsonify
from flask import g
def user_search(username_prefix: str) -> flask.wrappers.Response:
"""User_search."""
found_users = UserModel.query.filter(UserModel.username.like(f"{username_prefix}%")).all() # type: ignore
response_json = {
"users": found_users,
"username_prefix": username_prefix,
}
return make_response(jsonify(response_json), 200)
def user_group_list_for_current_user() -> flask.wrappers.Response:
"""User_group_list_for_current_user."""
groups = g.user.groups
# TODO: filter out the default group and have a way to know what is the default group
group_identifiers = [i.identifier for i in groups if i.identifier != "everybody"]
return make_response(jsonify(sorted(group_identifiers)), 200)

View File

@ -0,0 +1,41 @@
"""test_users_controller."""
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.user import UserModel
from flask.testing import FlaskClient
from flask.app import Flask
class TestUsersController(BaseTest):
"""TestUsersController."""
def test_user_search_returns_a_user(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_user_search_returns_a_user."""
self.find_or_create_user(username="aa")
self.find_or_create_user(username="ab")
self.find_or_create_user(username="abc")
self.find_or_create_user(username="ac")
self._assert_search_has_count(client, with_super_admin_user, 'aa', 1)
self._assert_search_has_count(client, with_super_admin_user, 'ab', 2)
self._assert_search_has_count(client, with_super_admin_user, 'ac', 1)
self._assert_search_has_count(client, with_super_admin_user, 'ad', 0)
self._assert_search_has_count(client, with_super_admin_user, 'a', 4)
def _assert_search_has_count(self, client: FlaskClient, with_super_admin_user: UserModel, username_prefix: str, expected_count: int) -> None:
"""_assert_search_has_count."""
response = client.get(
f"/v1.0/users/search?username_prefix={username_prefix}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json
assert response.json['users'] is not None
assert response.json['username_prefix'] == username_prefix
assert len(response.json['users']) == expected_count

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import {
Link,
useNavigate,
@ -60,6 +60,7 @@ import {
ReportColumnForEditing,
ReportMetadata,
ReportFilter,
User,
} from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
@ -134,10 +135,14 @@ export default function ProcessInstanceListTable({
const [errorObject, setErrorObject] = (useContext as any)(ErrorContext);
const processInstancePathPrefix =
const processInstanceListPathPrefix =
variant === 'all'
? '/admin/process-instances/all'
: '/admin/process-instances/for-me';
const processInstanceShowPathPrefix =
variant === 'all'
? '/admin/process-instances'
: '/admin/process-instances/for-me';
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[]
@ -164,6 +169,12 @@ export default function ProcessInstanceListTable({
useState<ReportColumnForEditing | null>(null);
const [reportColumnFormMode, setReportColumnFormMode] = useState<string>('');
const [processInstanceInitiatorOptions, setProcessInstanceInitiatorOptions] =
useState<string[]>([]);
const [processInitiatorSelection, setProcessInitiatorSelection] =
useState<User | null>(null);
const lastRequestedInitatorSearchTerm = useRef<string>();
const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return {
start_from: [setStartFromDate, setStartFromTime],
@ -529,7 +540,7 @@ export default function ProcessInstanceListTable({
setErrorObject(null);
setProcessInstanceReportJustSaved(null);
navigate(`${processInstancePathPrefix}?${queryParamString}`);
navigate(`${processInstanceListPathPrefix}?${queryParamString}`);
};
const dateComponent = (
@ -628,7 +639,7 @@ export default function ProcessInstanceListTable({
setErrorObject(null);
setProcessInstanceReportJustSaved(mode || null);
navigate(`${processInstancePathPrefix}${queryParamString}`);
navigate(`${processInstanceListPathPrefix}${queryParamString}`);
};
const reportColumns = () => {
@ -976,6 +987,22 @@ export default function ProcessInstanceListTable({
return null;
};
const handleProcessInstanceInitiatorSearchResult = (result: any) => {
if (lastRequestedInitatorSearchTerm.current === result.username_prefix) {
setProcessInstanceInitiatorOptions(result.users);
}
};
const searchForProcessInitiator = (inputText: string) => {
if (inputText) {
lastRequestedInitatorSearchTerm.current = inputText;
HttpService.makeCallToBackend({
path: `/users/search?username_prefix=${inputText}`,
successCallback: handleProcessInstanceInitiatorSearchResult,
});
}
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
@ -1008,7 +1035,27 @@ export default function ProcessInstanceListTable({
selectedItem={processModelSelection}
/>
</Column>
<Column md={8}>{processStatusSearch()}</Column>
<Column md={4}>
<ComboBox
onInputChange={searchForProcessInitiator}
onChange={(event: any) => {
setProcessInitiatorSelection(event.selectedItem);
}}
id="process-instance-initiator-search"
data-qa="process-instance-initiator-search"
items={processInstanceInitiatorOptions}
itemToString={(processInstanceInitatorOption: User) => {
if (processInstanceInitatorOption) {
return processInstanceInitatorOption.username;
}
return null;
}}
placeholder="Process Initiator"
titleText="PROC"
selectedItem={processInitiatorSelection}
/>
</Column>
<Column md={4}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
@ -1114,7 +1161,7 @@ export default function ProcessInstanceListTable({
return (
<Link
data-qa="process-instance-show-link"
to={`${processInstancePathPrefix}/${modifiedProcessModelId}/${id}`}
to={`${processInstanceShowPathPrefix}/${modifiedProcessModelId}/${id}`}
title={`View process instance ${id}`}
>
<span data-qa="paginated-entity-id">{id}</span>

View File

@ -1,3 +1,8 @@
export interface User {
id: number;
username: string;
}
export interface Secret {
id: number;
key: string;