Skip to content

Commit

Permalink
Add filters to task page (#479)
Browse files Browse the repository at this point in the history
  • Loading branch information
uittenbroekrobbert authored Jan 28, 2025
2 parents 967e36c + ff6daf2 commit f641ad5
Show file tree
Hide file tree
Showing 16 changed files with 502 additions and 198 deletions.
10 changes: 5 additions & 5 deletions amt/api/forms/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ async def get_measure_form(
name="measure_state",
label=_("Status"),
options=[
WebFormOption(value=MeasureStatusOptions.TODO, display_value="to do"),
WebFormOption(value=MeasureStatusOptions.IN_PROGRESS, display_value="in progress"),
WebFormOption(value=MeasureStatusOptions.IN_REVIEW, display_value="in review"),
WebFormOption(value=MeasureStatusOptions.DONE, display_value="done"),
WebFormOption(value=MeasureStatusOptions.NOT_IMPLEMENTED, display_value="not implemented"),
WebFormOption(value=MeasureStatusOptions.TODO, display_value=_("To do")),
WebFormOption(value=MeasureStatusOptions.IN_PROGRESS, display_value=_("In progress")),
WebFormOption(value=MeasureStatusOptions.IN_REVIEW, display_value=_("In review")),
WebFormOption(value=MeasureStatusOptions.DONE, display_value=_("Done")),
WebFormOption(value=MeasureStatusOptions.NOT_IMPLEMENTED, display_value=_("Not implemented")),
],
default_value=current_values.get("measure_state"),
group="1",
Expand Down
105 changes: 83 additions & 22 deletions amt/api/routes/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import urllib.parse
from collections import defaultdict
from collections.abc import Sequence
from typing import Annotated, Any
from typing import Annotated, Any, cast

import yaml
from fastapi import APIRouter, Depends, File, Form, Query, Request, Response, UploadFile
Expand All @@ -20,6 +20,7 @@
save_editable,
)
from amt.api.forms.measure import MeasureStatusOptions, get_measure_form
from amt.api.lifecycles import Lifecycles, get_localized_lifecycles
from amt.api.navigation import (
BaseNavigationItem,
Navigation,
Expand All @@ -31,11 +32,11 @@
from amt.core.authorization import AuthorizationResource, AuthorizationVerb, get_user
from amt.core.exceptions import AMTError, AMTNotFound, AMTPermissionDenied, AMTRepositoryError
from amt.core.internationalization import get_current_translation
from amt.enums.tasks import Status, TaskType, measure_state_to_status, status_mapper
from amt.enums.tasks import Status, TaskType, life_cycle_mapper, measure_state_to_status, status_mapper
from amt.models import Algorithm, User
from amt.models.task import Task
from amt.repositories.organizations import OrganizationsRepository
from amt.repositories.users import UsersRepository
from amt.schema.localized_value_item import LocalizedValueItem
from amt.schema.measure import ExtendedMeasureTask, MeasureTask, Person
from amt.schema.measure_display import DisplayMeasureTask
from amt.schema.requirement import RequirementTask
Expand Down Expand Up @@ -132,8 +133,14 @@ async def get_tasks(
algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)],
users_service: Annotated[UsersService, Depends(UsersService)],
tasks_service: Annotated[TasksService, Depends(TasksService)],
search: str = Query(""),
) -> HTMLResponse:
filters, drop_filters, localized_filters, _ = await get_filters_and_sort_by(request, users_service)
if search:
filters["search"] = search

algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request)

tab_items = get_algorithm_details_tabs(request)

tasks_db: Sequence[Task] = await tasks_service.get_tasks_for_algorithm(algorithm_id, None)
Expand All @@ -144,16 +151,53 @@ async def get_tasks(
{} if len(urns) == 0 else await resolve_and_enrich_measures(algorithm, urns, users_service)
)

def filters_match(display_task: DisplayTask) -> bool:
if len(filters) == 0:
return True
return all(
(
(
"assignee" not in filters
or any(
str(user.id) == filters.get("assignee")
for user in display_task.type_object.users # pyright: ignore[reportOptionalMemberAccess]
if "assignee" in filters and display_task.type_object is not None
)
),
(
"lifecycle" not in filters
or any(
lifecycle == life_cycle_mapper[Lifecycles(filters.get("lifecycle"))]
for lifecycle in display_task.type_object.lifecycle # pyright: ignore[reportOptionalMemberAccess]
if "lifecycle" in filters and display_task.type_object is not None
)
),
(
"search" not in filters
or (
"search" in filters
and display_task.type_object is not None
and (
cast(str, filters["search"]).casefold() in display_task.type_object.name.casefold() # pyright: ignore[reportOptionalMemberAccess]
or cast(str, filters["search"]).casefold()
in display_task.type_object.description.casefold() # pyright: ignore[reportOptionalMemberAccess]
)
)
),
)
)

tasks_by_status: dict[Status, list[DisplayTask]] = {}
for status in Status:
tasks_by_status[status] = []
tasks_by_status[status] += [
all_tasks = [
# we create display task for Measures,
# this could be extended in the future to support other types
DisplayTask.create_from_model(task, resolved_measures.get(task.type_id))
for task in tasks_db
if task.status_id == status and task.type == TaskType.MEASURE and task.type_id is not None
]
tasks_by_status[status] = [task for task in all_tasks if filters_match(task)]
# we also append all tasks that have no related object
tasks_by_status[status] += [
DisplayTask.create_from_model(task, None)
Expand All @@ -170,16 +214,30 @@ async def get_tasks(
request,
)

members = await users_service.find_all(filters={"organization-id": str(algorithm.organization.id)}) # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType]

context = {
"tasks_by_status": tasks_by_status,
"statuses": Status,
"algorithm": algorithm,
"algorithm_id": algorithm.id,
"breadcrumbs": breadcrumbs,
"tab_items": tab_items,
"base_href": f"/algorithm/{algorithm_id}/details/tasks",
"search": search,
"lifecycles": get_localized_lifecycles(request),
"assignees": [LocalizedValueItem(value=str(member.id), display_value=member.name) for member in members],
"filters": localized_filters,
}

return templates.TemplateResponse(request, "algorithms/tasks.html.j2", context)
headers = {"HX-Replace-Url": request.url.path + "?" + request.url.query}

if request.state.htmx and drop_filters:
return templates.TemplateResponse(request, "parts/tasks_search.html.j2", context, headers=headers)
elif request.state.htmx:
return templates.TemplateResponse(request, "parts/tasks_board.html.j2", context, headers=headers)
else:
return templates.TemplateResponse(request, "algorithms/tasks.html.j2", context, headers=headers)


async def resolve_and_enrich_measures(
Expand Down Expand Up @@ -214,6 +272,7 @@ async def resolve_and_enrich_measures(
value=system_measure.value,
version=system_measure.version,
users=all_users,
lifecycle=resolved_measure.lifecycle,
)
enriched_resolved_measures[system_measure.urn] = measure_task_display
return enriched_resolved_measures
Expand Down Expand Up @@ -481,14 +540,14 @@ async def get_system_card_requirements(
request: Request,
algorithm_id: int,
organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)],
users_repository: Annotated[UsersRepository, Depends(UsersRepository)],
users_service: Annotated[UsersService, Depends(UsersService)],
algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)],
) -> HTMLResponse:
algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request)
instrument_state = await get_instrument_state(algorithm.system_card)
requirements_state = await get_requirements_state(algorithm.system_card)
tab_items = get_algorithm_details_tabs(request)
filters, _, _, _ = get_filters_and_sort_by(request)
filters, _, _, _ = await get_filters_and_sort_by(request, users_service)
organization = await organizations_repository.find_by_id(algorithm.organization_id)
filters["organization-id"] = str(organization.id)

Expand Down Expand Up @@ -530,7 +589,7 @@ async def get_system_card_requirements(
extended_linked_measures.append(ext_measure_task)
requirements_and_measures.append((requirement, completed_measures_count, extended_linked_measures)) # pyright: ignore [reportUnknownMemberType]

measure_task_functions: dict[str, list[User]] = await get_measure_task_functions(measure_tasks, users_repository)
measure_task_functions: dict[str, list[User]] = await get_measure_task_functions(measure_tasks, users_service)

context = {
"instrument_state": instrument_state,
Expand All @@ -548,7 +607,7 @@ async def get_system_card_requirements(

async def get_measure_task_functions(
measure_tasks: list[MeasureTask],
users_repository: Annotated[UsersRepository, Depends(UsersRepository)],
users_service: Annotated[UsersService, Depends(UsersService)],
) -> dict[str, list[User]]:
measure_task_functions: dict[str, list[User]] = defaultdict(list)

Expand All @@ -557,7 +616,7 @@ async def get_measure_task_functions(
for person_type in person_types:
person_list = getattr(measure_task, person_type)
if person_list:
member = await users_repository.find_by_id(person_list[0].uuid)
member = await users_service.find_by_id(person_list[0].uuid)
if member:
measure_task_functions[measure_task.urn].append(member)
return measure_task_functions
Expand Down Expand Up @@ -611,15 +670,15 @@ async def delete_algorithm(
async def get_measure(
request: Request,
organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)],
users_repository: Annotated[UsersRepository, Depends(UsersRepository)],
users_service: Annotated[UsersService, Depends(UsersService)],
algorithm_id: int,
measure_urn: str,
algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)],
object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)],
search: str = Query(""),
requirement_urn: str = "",
) -> HTMLResponse:
filters, _, _, sort_by = get_filters_and_sort_by(request)
filters, _, _, sort_by = await get_filters_and_sort_by(request, users_service)
algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request)
measure = await measures_service.fetch_measures([measure_urn])
measure_task = get_measure_task_or_error(algorithm.system_card, measure_urn)
Expand All @@ -631,7 +690,7 @@ async def get_measure(

organization = await organizations_repository.find_by_id(algorithm.organization_id)
filters["organization-id"] = str(organization.id)
members = await users_repository.find_all(search=search, sort=sort_by, filters=filters)
members = await users_service.find_all(search=search, sort=sort_by, filters=filters)

measure_accountable = measure_task.accountable_persons[0].name if measure_task.accountable_persons else "" # pyright: ignore [reportOptionalMemberAccess]
measure_reviewer = measure_task.reviewer_persons[0].name if measure_task.reviewer_persons else "" # pyright: ignore [reportOptionalMemberAccess]
Expand Down Expand Up @@ -666,19 +725,19 @@ async def get_users_from_function_name(
measure_accountable: Annotated[str | None, Form()],
measure_reviewer: Annotated[str | None, Form()],
measure_responsible: Annotated[str | None, Form()],
users_repository: Annotated[UsersRepository, Depends(UsersRepository)],
users_service: Annotated[UsersService, Depends(UsersService)],
sort_by: dict[str, str],
filters: dict[str, str | list[str | int]],
) -> tuple[list[Person], list[Person], list[Person]]:
accountable_persons, reviewer_persons, responsible_persons = [], [], []
if measure_accountable:
accountable_member = await users_repository.find_all(search=measure_accountable, sort=sort_by, filters=filters)
accountable_member = await users_service.find_all(search=measure_accountable, sort=sort_by, filters=filters)
accountable_persons = [Person(name=accountable_member[0].name, uuid=str(accountable_member[0].id))] # pyright: ignore [reportOptionalMemberAccess]
if measure_reviewer:
reviewer_member = await users_repository.find_all(search=measure_reviewer, sort=sort_by, filters=filters)
reviewer_member = await users_service.find_all(search=measure_reviewer, sort=sort_by, filters=filters)
reviewer_persons = [Person(name=reviewer_member[0].name, uuid=str(reviewer_member[0].id))] # pyright: ignore [reportOptionalMemberAccess]
if measure_responsible:
responsible_member = await users_repository.find_all(search=measure_responsible, sort=sort_by, filters=filters)
responsible_member = await users_service.find_all(search=measure_responsible, sort=sort_by, filters=filters)
responsible_persons = [Person(name=responsible_member[0].name, uuid=str(responsible_member[0].id))] # pyright: ignore [reportOptionalMemberAccess]
return accountable_persons, reviewer_persons, responsible_persons

Expand All @@ -689,7 +748,7 @@ async def update_measure_value(
request: Request,
algorithm_id: int,
measure_urn: str,
users_repository: Annotated[UsersRepository, Depends(UsersRepository)],
users_service: Annotated[UsersService, Depends(UsersService)],
algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)],
tasks_service: Annotated[TasksService, Depends(TasksService)],
object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)],
Expand All @@ -702,7 +761,7 @@ async def update_measure_value(
measure_files: Annotated[list[UploadFile] | None, File()] = None,
requirement_urn: str = "",
) -> HTMLResponse:
filters, _, _, sort_by = get_filters_and_sort_by(request)
filters, _, _, sort_by = await get_filters_and_sort_by(request, users_service)
algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request)
user_id = get_user_id_or_error(request)
measure_task = get_measure_task_or_error(algorithm.system_card, measure_urn)
Expand All @@ -714,7 +773,7 @@ async def update_measure_value(
else None
)
accountable_persons, reviewer_persons, responsible_persons = await get_users_from_function_name(
measure_accountable, measure_reviewer, measure_responsible, users_repository, sort_by, filters
measure_accountable, measure_reviewer, measure_responsible, users_service, sort_by, filters
)

measure_task.update(
Expand All @@ -734,8 +793,10 @@ async def update_measure_value(
encoded_url = urllib.parse.quote_plus(
f"/algorithm/{algorithm_id}/details/system_card/compliance#{requirement_urn.replace(":","_")}"
)
if request.headers.get("referer", "").endswith("/details/tasks"):
encoded_url = urllib.parse.urlparse(request.headers.get("referer")).path
referer = urllib.parse.urlparse(request.headers.get("referer", ""))

if referer.path.endswith("/details/tasks"):
encoded_url = urllib.parse.quote_plus(referer.path + "?" + referer.query)
return templates.Redirect(
request,
f"/algorithm/{algorithm_id}/redirect?to={encoded_url}",
Expand Down
4 changes: 3 additions & 1 deletion amt/api/routes/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from amt.schema.webform import WebForm
from amt.services.algorithms import AlgorithmsService, get_template_files
from amt.services.organizations import OrganizationsService
from amt.services.users import UsersService

router = APIRouter()
logger = logging.getLogger(__name__)
Expand All @@ -33,12 +34,13 @@ async def get_root(
request: Request,
algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)],
organizations_service: Annotated[OrganizationsService, Depends(OrganizationsService)],
users_service: Annotated[UsersService, Depends(UsersService)],
skip: int = Query(0, ge=0),
limit: int = Query(5000, ge=1), # todo: fix infinite scroll
search: str = Query(""),
display_type: str = Query(""),
) -> HTMLResponse:
filters, drop_filters, localized_filters, sort_by = get_filters_and_sort_by(request)
filters, drop_filters, localized_filters, sort_by = await get_filters_and_sort_by(request, users_service)

session_user = get_user(request)
user_id: str | None = session_user["sub"] if session_user else None # pyright: ignore[reportUnknownVariableType]
Expand Down
13 changes: 8 additions & 5 deletions amt/api/routes/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from amt.schema.organization import OrganizationNew, OrganizationUsers
from amt.services.algorithms import AlgorithmsService
from amt.services.organizations import OrganizationsService
from amt.services.users import UsersService

router = APIRouter()

Expand Down Expand Up @@ -95,11 +96,12 @@ async def get_new(
async def root(
request: Request,
organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)],
users_service: Annotated[UsersService, Depends(UsersService)],
skip: int = Query(0, ge=0),
limit: int = Query(5000, ge=1), # todo: fix infinite scroll
search: str = Query(""),
) -> HTMLResponse:
filters, drop_filters, localized_filters, sort_by = get_filters_and_sort_by(request)
filters, drop_filters, localized_filters, sort_by = await get_filters_and_sort_by(request, users_service)

user = get_user(request)

Expand Down Expand Up @@ -332,14 +334,15 @@ async def show_algorithms(
request: Request,
algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)],
organizations_service: Annotated[OrganizationsService, Depends(OrganizationsService)],
users_service: Annotated[UsersService, Depends(UsersService)],
organization_slug: str,
skip: int = Query(0, ge=0),
limit: int = Query(5000, ge=1), # todo: fix infinite scroll
search: str = Query(""),
display_type: str = Query(""),
) -> HTMLResponse:
organization = await get_organization_or_error(organizations_service, request, organization_slug)
filters, drop_filters, localized_filters, sort_by = get_filters_and_sort_by(request)
filters, drop_filters, localized_filters, sort_by = await get_filters_and_sort_by(request, users_service)

filters["organization-id"] = str(organization.id)
algorithms, amount_algorithm_systems = await get_algorithms(
Expand Down Expand Up @@ -434,13 +437,13 @@ async def get_members(
request: Request,
organization_slug: str,
organizations_service: Annotated[OrganizationsService, Depends(OrganizationsService)],
users_repository: Annotated[UsersRepository, Depends(UsersRepository)],
users_service: Annotated[UsersService, Depends(UsersService)],
skip: int = Query(0, ge=0),
limit: int = Query(5000, ge=1), # todo: fix infinite scroll
search: str = Query(""),
) -> HTMLResponse:
organization = await get_organization_or_error(organizations_service, request, organization_slug)
filters, drop_filters, localized_filters, sort_by = get_filters_and_sort_by(request)
filters, drop_filters, localized_filters, sort_by = await get_filters_and_sort_by(request, users_service)
tab_items = get_organization_tabs(request, organization_slug=organization_slug)
breadcrumbs = resolve_base_navigation_items(
[
Expand All @@ -452,7 +455,7 @@ async def get_members(
)

filters["organization-id"] = str(organization.id)
members = await users_repository.find_all(search=search, sort=sort_by, filters=filters)
members = await users_service.find_all(search=search, sort=sort_by, filters=filters)

context: dict[str, Any] = {
"slug": organization.slug,
Expand Down
Loading

0 comments on commit f641ad5

Please sign in to comment.