diff --git a/amt/api/forms/measure.py b/amt/api/forms/measure.py index c4868627..2e147c28 100644 --- a/amt/api/forms/measure.py +++ b/amt/api/forms/measure.py @@ -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", diff --git a/amt/api/routes/algorithm.py b/amt/api/routes/algorithm.py index 93602550..4a8dac75 100644 --- a/amt/api/routes/algorithm.py +++ b/amt/api/routes/algorithm.py @@ -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 @@ -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, @@ -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 @@ -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) @@ -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) @@ -170,6 +214,8 @@ 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, @@ -177,9 +223,21 @@ async def get_tasks( "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( @@ -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 @@ -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) @@ -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, @@ -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) @@ -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 @@ -611,7 +670,7 @@ 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)], @@ -619,7 +678,7 @@ async def get_measure( 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) @@ -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] @@ -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 @@ -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)], @@ -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) @@ -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( @@ -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}", diff --git a/amt/api/routes/algorithms.py b/amt/api/routes/algorithms.py index 17439d19..5d6b8018 100644 --- a/amt/api/routes/algorithms.py +++ b/amt/api/routes/algorithms.py @@ -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__) @@ -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] diff --git a/amt/api/routes/organizations.py b/amt/api/routes/organizations.py index 3f8dbd34..f67436af 100644 --- a/amt/api/routes/organizations.py +++ b/amt/api/routes/organizations.py @@ -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() @@ -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) @@ -332,6 +334,7 @@ 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 @@ -339,7 +342,7 @@ async def show_algorithms( 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( @@ -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( [ @@ -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, diff --git a/amt/api/routes/shared.py b/amt/api/routes/shared.py index d689e3b3..3dcbf0b7 100644 --- a/amt/api/routes/shared.py +++ b/amt/api/routes/shared.py @@ -10,10 +10,11 @@ from amt.api.risk_group import RiskGroup, get_localized_risk_group from amt.schema.localized_value_item import LocalizedValueItem from amt.schema.shared import IterMixin +from amt.services.users import UsersService -def get_filters_and_sort_by( - request: Request, +async def get_filters_and_sort_by( + request: Request, users_service: UsersService ) -> tuple[dict[str, str | list[str | int]], list[str], dict[str, LocalizedValueItem], dict[str, str]]: active_filters: dict[str, str] = { k.removeprefix("active-filter-"): v @@ -32,9 +33,15 @@ def get_filters_and_sort_by( filters: dict[str, str | list[str | int]] = { k: v for k, v in (active_filters | add_filters).items() if k not in drop_filters } - localized_filters: dict[str, LocalizedValueItem] = { - k: get_localized_value(k, cast(str, v), request) for k, v in filters.items() - } + localized_filters: dict[str, LocalizedValueItem] = {} + if "assignee" in filters: + user_id = filters.get("assignee") + user = await users_service.find_by_id(cast(str, user_id)) + if user is not None: + localized_filters["assignee"] = LocalizedValueItem(display_value=user.name, value=str(user_id)) + localized_filters.update( + {k: get_localized_value(k, cast(str, v), request) for k, v in filters.items() if k != "assignee"} + ) sort_by: dict[str, str] = { k.removeprefix("sort-by-"): v for k, v in request.query_params.items() if k.startswith("sort-by-") and v != "" } diff --git a/amt/enums/tasks.py b/amt/enums/tasks.py index 88c55dfd..b770c4a9 100644 --- a/amt/enums/tasks.py +++ b/amt/enums/tasks.py @@ -1,6 +1,7 @@ from enum import IntEnum, StrEnum from amt.api.forms.measure import MeasureStatusOptions +from amt.api.lifecycles import Lifecycles class Status(IntEnum): @@ -26,3 +27,16 @@ class TaskType(StrEnum): def measure_state_to_status(state: str) -> Status: return next((k for k, v in status_mapper.items() if v.value == state), Status.TODO) + + +life_cycle_mapper: dict[Lifecycles, str] = { + Lifecycles.ORGANIZATIONAL_RESPONSIBILITIES: "organisatieverantwoordelijkheden", + Lifecycles.PROBLEM_ANALYSIS: "probleemanalyse", + Lifecycles.DESIGN: "ontwerp", + Lifecycles.DATA_EXPLORATION_AND_PREPARATION: "dataverkenning en datapreparatie", + Lifecycles.DEVELOPMENT: "ontwikkelen", + Lifecycles.VERIFICATION_AND_VALIDATION: "verificatie en validatie", + Lifecycles.IMPLEMENTATION: "implementatie", + Lifecycles.MONITORING_AND_MANAGEMENT: "monitoring en beheer", + Lifecycles.PHASING_OUT: "uitfaseren", +} diff --git a/amt/locale/base.pot b/amt/locale/base.pot index 23479e81..aad8ea4c 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -50,6 +50,7 @@ msgstr "" #: amt/site/templates/algorithms/new.html.j2:41 #: amt/site/templates/parts/algorithm_search.html.j2:48 #: amt/site/templates/parts/filter_list.html.j2:71 +#: amt/site/templates/parts/tasks_search.html.j2:41 msgid "Lifecycle" msgstr "" @@ -217,6 +218,33 @@ msgstr "" msgid "Status" msgstr "" +#: amt/api/forms/measure.py:59 +#: amt/site/templates/algorithms/details_info.html.j2:44 +#: amt/site/templates/macros/tasks.html.j2:12 +msgid "To do" +msgstr "" + +#: amt/api/forms/measure.py:60 +#: amt/site/templates/algorithms/details_info.html.j2:46 +#: amt/site/templates/macros/tasks.html.j2:19 +msgid "In progress" +msgstr "" + +#: amt/api/forms/measure.py:61 amt/site/templates/macros/tasks.html.j2:26 +msgid "In review" +msgstr "" + +#: amt/api/forms/measure.py:62 +#: amt/site/templates/algorithms/details_info.html.j2:20 +#: amt/site/templates/algorithms/details_info.html.j2:48 +#: amt/site/templates/macros/tasks.html.j2:33 +msgid "Done" +msgstr "" + +#: amt/api/forms/measure.py:63 amt/site/templates/macros/tasks.html.j2:40 +msgid "Not implemented" +msgstr "" + #: amt/api/forms/measure.py:72 msgid "Information on how this measure is implemented" msgstr "" @@ -388,7 +416,7 @@ msgstr "" #: amt/site/templates/algorithms/details_compliance.html.j2:69 #: amt/site/templates/macros/editable.html.j2:28 #: amt/site/templates/macros/editable.html.j2:33 -#: amt/site/templates/macros/tasks.html.j2:81 +#: amt/site/templates/macros/tasks.html.j2:82 msgid "Edit" msgstr "" @@ -396,22 +424,6 @@ msgstr "" msgid "Does the algorithm meet the requirements?" msgstr "" -#: amt/site/templates/algorithms/details_info.html.j2:20 -#: amt/site/templates/algorithms/details_info.html.j2:48 -#: amt/site/templates/macros/tasks.html.j2:33 -msgid "Done" -msgstr "" - -#: amt/site/templates/algorithms/details_info.html.j2:44 -#: amt/site/templates/macros/tasks.html.j2:12 -msgid "To do" -msgstr "" - -#: amt/site/templates/algorithms/details_info.html.j2:46 -#: amt/site/templates/macros/tasks.html.j2:19 -msgid "In progress" -msgstr "" - #: amt/site/templates/algorithms/details_info.html.j2:65 msgid "Go to all requirements" msgstr "" @@ -644,14 +656,6 @@ msgstr "" msgid " ago" msgstr "" -#: amt/site/templates/macros/tasks.html.j2:26 -msgid "In review" -msgstr "" - -#: amt/site/templates/macros/tasks.html.j2:40 -msgid "Not implemented" -msgstr "" - #: amt/site/templates/macros/tasks.html.j2:43 msgid "Unknown" msgstr "" @@ -684,6 +688,7 @@ msgstr "" #: amt/site/templates/organizations/parts/members_results.html.j2:29 #: amt/site/templates/organizations/parts/overview_results.html.j2:26 #: amt/site/templates/parts/algorithm_search.html.j2:25 +#: amt/site/templates/parts/tasks_search.html.j2:18 msgid "Search" msgstr "" @@ -836,6 +841,7 @@ msgid "Find algorithm..." msgstr "" #: amt/site/templates/parts/algorithm_search.html.j2:53 +#: amt/site/templates/parts/tasks_search.html.j2:46 msgid "Select lifecycle" msgstr "" @@ -890,3 +896,23 @@ msgid "" " open manner." msgstr "" +#: amt/site/templates/parts/tasks_board.html.j2:5 +msgid "Results for" +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:9 +msgid "Taken" +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:25 +msgid "Find tasks..." +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:55 +msgid "Assignee" +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:60 +msgid "Select assignee" +msgstr "" + diff --git a/amt/locale/en_US/LC_MESSAGES/messages.mo b/amt/locale/en_US/LC_MESSAGES/messages.mo index 738f59d7..a93e9e28 100644 Binary files a/amt/locale/en_US/LC_MESSAGES/messages.mo and b/amt/locale/en_US/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/en_US/LC_MESSAGES/messages.po b/amt/locale/en_US/LC_MESSAGES/messages.po index 2e04e955..21db9509 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -51,6 +51,7 @@ msgstr "" #: amt/site/templates/algorithms/new.html.j2:41 #: amt/site/templates/parts/algorithm_search.html.j2:48 #: amt/site/templates/parts/filter_list.html.j2:71 +#: amt/site/templates/parts/tasks_search.html.j2:41 msgid "Lifecycle" msgstr "" @@ -218,6 +219,33 @@ msgstr "" msgid "Status" msgstr "" +#: amt/api/forms/measure.py:59 +#: amt/site/templates/algorithms/details_info.html.j2:44 +#: amt/site/templates/macros/tasks.html.j2:12 +msgid "To do" +msgstr "" + +#: amt/api/forms/measure.py:60 +#: amt/site/templates/algorithms/details_info.html.j2:46 +#: amt/site/templates/macros/tasks.html.j2:19 +msgid "In progress" +msgstr "" + +#: amt/api/forms/measure.py:61 amt/site/templates/macros/tasks.html.j2:26 +msgid "In review" +msgstr "" + +#: amt/api/forms/measure.py:62 +#: amt/site/templates/algorithms/details_info.html.j2:20 +#: amt/site/templates/algorithms/details_info.html.j2:48 +#: amt/site/templates/macros/tasks.html.j2:33 +msgid "Done" +msgstr "" + +#: amt/api/forms/measure.py:63 amt/site/templates/macros/tasks.html.j2:40 +msgid "Not implemented" +msgstr "" + #: amt/api/forms/measure.py:72 msgid "Information on how this measure is implemented" msgstr "" @@ -389,7 +417,7 @@ msgstr "" #: amt/site/templates/algorithms/details_compliance.html.j2:69 #: amt/site/templates/macros/editable.html.j2:28 #: amt/site/templates/macros/editable.html.j2:33 -#: amt/site/templates/macros/tasks.html.j2:81 +#: amt/site/templates/macros/tasks.html.j2:82 msgid "Edit" msgstr "" @@ -397,22 +425,6 @@ msgstr "" msgid "Does the algorithm meet the requirements?" msgstr "" -#: amt/site/templates/algorithms/details_info.html.j2:20 -#: amt/site/templates/algorithms/details_info.html.j2:48 -#: amt/site/templates/macros/tasks.html.j2:33 -msgid "Done" -msgstr "" - -#: amt/site/templates/algorithms/details_info.html.j2:44 -#: amt/site/templates/macros/tasks.html.j2:12 -msgid "To do" -msgstr "" - -#: amt/site/templates/algorithms/details_info.html.j2:46 -#: amt/site/templates/macros/tasks.html.j2:19 -msgid "In progress" -msgstr "" - #: amt/site/templates/algorithms/details_info.html.j2:65 msgid "Go to all requirements" msgstr "" @@ -645,14 +657,6 @@ msgstr "" msgid " ago" msgstr "" -#: amt/site/templates/macros/tasks.html.j2:26 -msgid "In review" -msgstr "" - -#: amt/site/templates/macros/tasks.html.j2:40 -msgid "Not implemented" -msgstr "" - #: amt/site/templates/macros/tasks.html.j2:43 msgid "Unknown" msgstr "" @@ -685,6 +689,7 @@ msgstr "" #: amt/site/templates/organizations/parts/members_results.html.j2:29 #: amt/site/templates/organizations/parts/overview_results.html.j2:26 #: amt/site/templates/parts/algorithm_search.html.j2:25 +#: amt/site/templates/parts/tasks_search.html.j2:18 msgid "Search" msgstr "" @@ -837,6 +842,7 @@ msgid "Find algorithm..." msgstr "" #: amt/site/templates/parts/algorithm_search.html.j2:53 +#: amt/site/templates/parts/tasks_search.html.j2:46 msgid "Select lifecycle" msgstr "" @@ -891,3 +897,23 @@ msgid "" " open manner." msgstr "" +#: amt/site/templates/parts/tasks_board.html.j2:5 +msgid "Results for" +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:9 +msgid "Taken" +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:25 +msgid "Find tasks..." +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:55 +msgid "Assignee" +msgstr "" + +#: amt/site/templates/parts/tasks_search.html.j2:60 +msgid "Select assignee" +msgstr "" + diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.mo b/amt/locale/nl_NL/LC_MESSAGES/messages.mo index fb3c3283..6d5eccc6 100644 Binary files a/amt/locale/nl_NL/LC_MESSAGES/messages.mo and b/amt/locale/nl_NL/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.po b/amt/locale/nl_NL/LC_MESSAGES/messages.po index c687ac84..2db7ea21 100644 --- a/amt/locale/nl_NL/LC_MESSAGES/messages.po +++ b/amt/locale/nl_NL/LC_MESSAGES/messages.po @@ -53,6 +53,7 @@ msgstr "Rol" #: amt/site/templates/algorithms/new.html.j2:41 #: amt/site/templates/parts/algorithm_search.html.j2:48 #: amt/site/templates/parts/filter_list.html.j2:71 +#: amt/site/templates/parts/tasks_search.html.j2:41 msgid "Lifecycle" msgstr "Levenscyclus" @@ -220,6 +221,33 @@ msgstr "Verantwoordelijke" msgid "Status" msgstr "Status" +#: amt/api/forms/measure.py:59 +#: amt/site/templates/algorithms/details_info.html.j2:44 +#: amt/site/templates/macros/tasks.html.j2:12 +msgid "To do" +msgstr "Te doen" + +#: amt/api/forms/measure.py:60 +#: amt/site/templates/algorithms/details_info.html.j2:46 +#: amt/site/templates/macros/tasks.html.j2:19 +msgid "In progress" +msgstr "Onderhanden" + +#: amt/api/forms/measure.py:61 amt/site/templates/macros/tasks.html.j2:26 +msgid "In review" +msgstr "Beoordelen" + +#: amt/api/forms/measure.py:62 +#: amt/site/templates/algorithms/details_info.html.j2:20 +#: amt/site/templates/algorithms/details_info.html.j2:48 +#: amt/site/templates/macros/tasks.html.j2:33 +msgid "Done" +msgstr "Afgerond" + +#: amt/api/forms/measure.py:63 amt/site/templates/macros/tasks.html.j2:40 +msgid "Not implemented" +msgstr "Niet van toepassing" + #: amt/api/forms/measure.py:72 msgid "Information on how this measure is implemented" msgstr "Informatie over hoe deze maatregel is geïmplementeerd" @@ -408,7 +436,7 @@ msgstr "maatregelen uitgevoerd" #: amt/site/templates/algorithms/details_compliance.html.j2:69 #: amt/site/templates/macros/editable.html.j2:28 #: amt/site/templates/macros/editable.html.j2:33 -#: amt/site/templates/macros/tasks.html.j2:81 +#: amt/site/templates/macros/tasks.html.j2:82 msgid "Edit" msgstr "Bewerk" @@ -416,22 +444,6 @@ msgstr "Bewerk" msgid "Does the algorithm meet the requirements?" msgstr "Voldoet het algoritme aan de vereisten?" -#: amt/site/templates/algorithms/details_info.html.j2:20 -#: amt/site/templates/algorithms/details_info.html.j2:48 -#: amt/site/templates/macros/tasks.html.j2:33 -msgid "Done" -msgstr "Afgerond" - -#: amt/site/templates/algorithms/details_info.html.j2:44 -#: amt/site/templates/macros/tasks.html.j2:12 -msgid "To do" -msgstr "Te doen" - -#: amt/site/templates/algorithms/details_info.html.j2:46 -#: amt/site/templates/macros/tasks.html.j2:19 -msgid "In progress" -msgstr "Onderhanden" - #: amt/site/templates/algorithms/details_info.html.j2:65 msgid "Go to all requirements" msgstr "Ga naar alle Vereisten" @@ -667,14 +679,6 @@ msgstr "Weet u zeker dat u het bestand wilt verwijderen" msgid " ago" msgstr "geleden" -#: amt/site/templates/macros/tasks.html.j2:26 -msgid "In review" -msgstr "Beoordelen" - -#: amt/site/templates/macros/tasks.html.j2:40 -msgid "Not implemented" -msgstr "Niet van toepassing" - #: amt/site/templates/macros/tasks.html.j2:43 msgid "Unknown" msgstr "Onbekend" @@ -707,6 +711,7 @@ msgstr "Voeg personen toe" #: amt/site/templates/organizations/parts/members_results.html.j2:29 #: amt/site/templates/organizations/parts/overview_results.html.j2:26 #: amt/site/templates/parts/algorithm_search.html.j2:25 +#: amt/site/templates/parts/tasks_search.html.j2:18 msgid "Search" msgstr "Zoek" @@ -866,6 +871,7 @@ msgid "Find algorithm..." msgstr "Vind algoritme..." #: amt/site/templates/parts/algorithm_search.html.j2:53 +#: amt/site/templates/parts/tasks_search.html.j2:46 msgid "Select lifecycle" msgstr "Selecteer levenscyclus" @@ -928,3 +934,23 @@ msgid "" " open manner." msgstr "Deze website is in ontwikkeling. Alle versies ontstaan op een open manier." +#: amt/site/templates/parts/tasks_board.html.j2:5 +msgid "Results for" +msgstr "Resultaten voor" + +#: amt/site/templates/parts/tasks_search.html.j2:9 +msgid "Taken" +msgstr "Taken" + +#: amt/site/templates/parts/tasks_search.html.j2:25 +msgid "Find tasks..." +msgstr "Vind taken..." + +#: amt/site/templates/parts/tasks_search.html.j2:55 +msgid "Assignee" +msgstr "Toegewezen aan" + +#: amt/site/templates/parts/tasks_search.html.j2:60 +msgid "Select assignee" +msgstr "Selecteer toegewezen aan" + diff --git a/amt/services/users.py b/amt/services/users.py index 086a64aa..dcff8103 100644 --- a/amt/services/users.py +++ b/amt/services/users.py @@ -1,4 +1,5 @@ import logging +from collections.abc import Sequence from typing import Annotated from uuid import UUID @@ -27,3 +28,13 @@ async def find_by_id(self, id: UUID | str) -> User | None: new_user = await self.repository.find_by_id(id) self.cache[id] = new_user return self.cache[id] + + async def find_all( + self, + search: str | None = None, + sort: dict[str, str] | None = None, + filters: dict[str, str | list[str | int]] | None = None, + skip: int | None = None, + limit: int | None = None, + ) -> Sequence[User]: + return await self.repository.find_all(search, sort, filters, skip, limit) diff --git a/amt/site/static/ts/amt.ts b/amt/site/static/ts/amt.ts index 3ed28b8b..bbe1f168 100644 --- a/amt/site/static/ts/amt.ts +++ b/amt/site/static/ts/amt.ts @@ -8,27 +8,45 @@ import "../scss/layout.scss"; _hyperscript.browserInit(); -const keysToRemove = [ - "labelsbysubcategory", - "answers", - "categoryState", - "categoryTrace", - "currentCategory", - "currentconclusion", - "currentquestion", - "currentSubCategory", - "labels", - "previousCategory", - "previousSubCategory", - "subCategoryTrace", -]; +const currentSortables: Sortable[] = []; + if (window.location.pathname === "/algorithms/new") { + const keysToRemove = [ + "labelsbysubcategory", + "answers", + "categoryState", + "categoryTrace", + "currentCategory", + "currentconclusion", + "currentquestion", + "currentSubCategory", + "labels", + "previousCategory", + "previousSubCategory", + "subCategoryTrace", + ]; keysToRemove.forEach((key) => { sessionStorage.removeItem(key); }); } -document.addEventListener("DOMContentLoaded", function () { +document.addEventListener("htmx:afterSwap", (e) => { + if ( + ["tasks-search-results", "search-tasks-container"].includes( + (e.target as HTMLElement).getAttribute("id") as string, + ) + ) { + while (currentSortables.length > 0) { + const sortable = currentSortables.shift(); + sortable!.destroy(); + } + initPage(); + } +}); + +document.addEventListener("DOMContentLoaded", initPage); + +function initPage() { // TODO (robbert): we need (better) event handling and displaying of server errors document.body.addEventListener("htmx:sendError", function () { document.getElementById("errorContainer")!.innerHTML = @@ -40,44 +58,45 @@ document.addEventListener("DOMContentLoaded", function () { ) as HTMLCollectionOf; for (const column of columns) { // prettier-ignore - new Sortable(column, { //NOSONAR - group: "shared", // set both lists to same group - animation: 150, - onEnd: function (evt) { - if (evt.oldIndex !== evt.newIndex || evt.from !== evt.to) { - const previousSiblingId = evt.item.previousElementSibling - ? evt.item.previousElementSibling.getAttribute("data-id") - : "-1"; - const nextSiblingId = evt.item.nextElementSibling - ? evt.item.nextElementSibling.getAttribute("data-id") - : "-1"; - const targetId = "#" + evt.item.getAttribute("data-target-id"); - const toStatusId = evt.to.getAttribute("data-id"); - const form = (document.getElementById("cardMovedForm") ?? - "") as HTMLFormElement; - - (document.getElementsByName("taskId")[0] as HTMLInputElement).value = - evt.item.getAttribute("data-id") ?? ""; - ( - document.getElementsByName("statusId")[0] as HTMLInputElement - ).value = toStatusId ?? ""; - ( - document.getElementsByName( - "previousSiblingId", - )[0] as HTMLInputElement - ).value = previousSiblingId ?? ""; - ( - document.getElementsByName("nextSiblingId")[0] as HTMLInputElement - ).value = nextSiblingId ?? ""; - form.setAttribute("hx-target", targetId); - - // @ts-expect-error Description: Ignoring type error because the htmx.trigger function is not recognized by TypeScript. - htmx.trigger("#cardMovedForm", "cardmoved"); - } - }, - }); + currentSortables.push(Sortable.create(column, { //NOSONAR + group: "shared", // set both lists to same group + animation: 150, + onEnd: function (evt) { + if (evt.oldIndex !== evt.newIndex || evt.from !== evt.to) { + const previousSiblingId = evt.item.previousElementSibling + ? evt.item.previousElementSibling.getAttribute("data-id") + : "-1"; + const nextSiblingId = evt.item.nextElementSibling + ? evt.item.nextElementSibling.getAttribute("data-id") + : "-1"; + const targetId = "#" + evt.item.getAttribute("data-target-id"); + const toStatusId = evt.to.getAttribute("data-id"); + const form = (document.getElementById("cardMovedForm") ?? + "") as HTMLFormElement; + + (document.getElementsByName("taskId")[0] as HTMLInputElement).value = + evt.item.getAttribute("data-id") ?? ""; + ( + document.getElementsByName("statusId")[0] as HTMLInputElement + ).value = toStatusId ?? ""; + ( + document.getElementsByName( + "previousSiblingId", + )[0] as HTMLInputElement + ).value = previousSiblingId ?? ""; + ( + document.getElementsByName("nextSiblingId")[0] as HTMLInputElement + ).value = nextSiblingId ?? ""; + form.setAttribute("hx-target", targetId); + + // @ts-expect-error Description: Ignoring type error because the htmx.trigger function is not recognized by TypeScript. + htmx.trigger("#cardMovedForm", "cardmoved"); + } + }, + }) + ) } -}); +} export function setCookie( cookieName: string, diff --git a/amt/site/templates/algorithms/tasks.html.j2 b/amt/site/templates/algorithms/tasks.html.j2 index ca826114..41de3527 100644 --- a/amt/site/templates/algorithms/tasks.html.j2 +++ b/amt/site/templates/algorithms/tasks.html.j2 @@ -1,35 +1,6 @@ -{% extends 'algorithms/details_base.html.j2' %} -{# disable macro caching, see // https://jinja.palletsprojects.com/en/3.0.x/templates/#import-context-behavior #} -{% import 'macros/tasks.html.j2' as render with context %} -{% block detail_content %} - -
-
- - - - -
-
-
-
- {% for status in statuses %}{{ render.column(algorithm, status, translations, tasks_by_status) }}{% endfor %} -
-
+{% extends 'layouts/base.html.j2' %} +{% block content %} +
+
{% include 'parts/tasks_search.html.j2' %}
{% endblock %} diff --git a/amt/site/templates/parts/tasks_board.html.j2 b/amt/site/templates/parts/tasks_board.html.j2 new file mode 100644 index 00000000..004ee9e7 --- /dev/null +++ b/amt/site/templates/parts/tasks_board.html.j2 @@ -0,0 +1,63 @@ +{% import 'macros/tasks.html.j2' as render with context %} +{% if search or filters %} +
+
+ {% trans %}Results for{% endtrans %} + {% if search %}'{{ search }}'{% endif %} +
+ + {% for key, localized_value in filters.items() %} + + + {{ localized_value.display_value }} + + + {% endfor %} +
+
+
+{% endif %} + +
+
+ + + + +
+
+
+
+ {% for status in statuses %}{{ render.column(algorithm, status, translations, tasks_by_status) }}{% endfor %} +
+
+
diff --git a/amt/site/templates/parts/tasks_search.html.j2 b/amt/site/templates/parts/tasks_search.html.j2 new file mode 100644 index 00000000..8143da64 --- /dev/null +++ b/amt/site/templates/parts/tasks_search.html.j2 @@ -0,0 +1,75 @@ +
+
+
+

{% trans %}Taken{% endtrans %}

+
+
+
+
+
+
+
+
+
{% trans %}Search{% endtrans %}
+
+ + +
+
+
+
{% trans %}Lifecycle{% endtrans %}
+
+ +
+
+
+
{% trans %}Assignee{% endtrans %}
+
+ +
+
+
+
+
+
+
+
+
{% include 'parts/tasks_board.html.j2' %}
+