From 4affbd82b2d1c0148f331439efc4fd7e4dee6269 Mon Sep 17 00:00:00 2001 From: robbertuittenbroek Date: Fri, 24 Jan 2025 10:02:13 +0100 Subject: [PATCH] Add tasks for measures when creating an algorithm # Conflicts: # amt/api/routes/algorithm.py # amt/enums/tasks.py # amt/locale/base.pot # amt/locale/en_US/LC_MESSAGES/messages.po # amt/locale/nl_NL/LC_MESSAGES/messages.mo # amt/locale/nl_NL/LC_MESSAGES/messages.po # amt/services/users.py # amt/site/templates/algorithms/tasks.html.j2 --- amt/api/forms/measure.py | 10 +- amt/api/routes/algorithm.py | 104 +++++++++++---- amt/api/routes/algorithms.py | 4 +- amt/api/routes/organizations.py | 13 +- amt/api/routes/shared.py | 17 ++- amt/enums/tasks.py | 14 ++ amt/locale/base.pot | 76 +++++++---- amt/locale/en_US/LC_MESSAGES/messages.po | 76 +++++++---- amt/locale/nl_NL/LC_MESSAGES/messages.mo | Bin 14357 -> 14574 bytes amt/locale/nl_NL/LC_MESSAGES/messages.po | 76 +++++++---- amt/services/users.py | 11 ++ amt/site/static/ts/amt.ts | 123 ++++++++++-------- amt/site/templates/algorithms/tasks.html.j2 | 37 +----- amt/site/templates/parts/tasks_board.html.j2 | 63 +++++++++ amt/site/templates/parts/tasks_search.html.j2 | 75 +++++++++++ 15 files changed, 501 insertions(+), 198 deletions(-) create mode 100644 amt/site/templates/parts/tasks_board.html.j2 create mode 100644 amt/site/templates/parts/tasks_search.html.j2 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..3280ca7d 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,52 @@ 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"]) in display_task.type_object.name # pyright: ignore[reportOptionalMemberAccess] + or cast(str, filters["search"]) in display_task.type_object.description # 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] = [t for t in all_tasks if filters_match(t)] # we also append all tasks that have no related object tasks_by_status[status] += [ DisplayTask.create_from_model(task, None) @@ -170,6 +213,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 +222,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 +271,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 +539,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 +588,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 +606,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 +615,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 +669,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 +677,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 +689,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 +724,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 +747,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 +760,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 +772,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 +792,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..ecce077a 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:6 +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.po b/amt/locale/en_US/LC_MESSAGES/messages.po index 2e04e955..757379b9 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:6 +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 fb3c32832c51fb7d3b1192a64182c21201a7acd0..8ea677665ab4c312049bd2b1c350e00c8824619d 100644 GIT binary patch delta 3356 zcmY+`drX&A9LMn^_xn}6AlWYw6;wnKFL|3nqL`Y7rGhM6KhZ!)!E_}(vP{uzE08R0 z4kfec=0?9QU5T2QQ%Q3ZrY%e7OxGXU%u7<51&_M%7QT z=3@!vC76fjFqQdDZ)Ta#jcineALEnQgrl()J=lr#F-eT7frs!Ff)kOIm|P?|W)8+- z5o){@sCH{G5Nl8YZp1j|H+5Vn1ACA;%wbjFS$qE~s>6M&AC=mINc3PFW@9F5!qwOd z-$Z3{8?q~=$(EZ@?OM>$UYxKM-(oK1OQ;lk$a^XdLru5<2V)T`(9QP#4pe}Rs1@$V z3~a_nTZP1K?%1+_cViML$8{(Fifj-!6!9q31hY{Yc>~8_9V&%wn2GJEfI=8W6GWm` znuOYlL8!n-pxPB;Iu@e`-$S);P9Xo<>lSY4H2;V?wf9j0^&twSY9uOvai~4cK}|Hr z))$~wzSx$RqB2`y@2^H>s2VkKt#zxzg(hx9rSwZw#BE3n<~%Bp8|aV!pfcv)(`_G% zew6!RHKriDXPQul^$S${ljy;A)K>g~T8IaqPRG_uC z{v%Ys-KZ5Fx1L2!a0%7_XX_o*xLwFX9TUXzwc>D8s$-Bvnx3dY(oriJZ++HUhzg_( zOR*X`U*;Fog1T@dMkl)C=im&=({U0uV33~w4lZ=4F5^Iq>ElK`2DPU-);#Rj&6wv< z0ao%h^5ACdhx_qEJcr6u$rHSMa06;9Poo07f_%(fzG9i*#L`+5C8IKtirT|;)FGN| z>!%|zn%PJU<`sN20qQWGMxBuhsDLio@?WT}d1%X_?AKJvk?1I*d@c%D*;3Twbi(7N z_5!N@I{M)QYbPpzhp4?xe2R3^e*kW#e3;quFqcP33#db7b~`?e$58=vC6j-pGN-@$ z*cG7m&Ov45ebgb^h6>~pB)jGis$GZmCMv~$q81RH;J*2utYH|B6EGVUXboz@2Grr*iwfi*YO7A7GI!qL zLK(PhEACpmFqr$ngWQ!wqlfYkWS7iL)C9{>D|e8WqG>=qHTzH#AHo1Ug&sVMi}5xp z5NA%R8*u?@zzS;(D)sdkijAm%_MsyG9ACsEsCGTl+&xal5Xv(!6Z27rxfX+P2P)9r z7^oy4u!$Y&{=<=ly1x*$!ZoOI9MnSA zqxyef>zhy++J~V!WIiqwVT-jD72#zzTHqINjqyr#7CWtceC9w@eo575g$OpjEb9gC5FtsFnFJ6px_-K7%RP zfzjBB3Mf3oJ-o4~@rGxRe|4C^4Gl2OR?J0pEJ6iTj*5IO>JY9&rMMOq_`9g~yO6&t zrWuu~GpKe~P-o^kuES2$Q?qtB`B%fb;l@0Jdr=*3;Vk?O=i%fL?%r-g`kI}{$9%_^ zGT_Hi*9xPp3Al%HAJjx&BX3`G5mWFU9>64rBdo*MhO_Z^REL}_caImMI+mdVeFv3+ zMvTMHP%Hlmb*S1p7;TECOUB}#&fdOeBeJN^vEJRr1kRasS~XGz7%`c1uZLy8}_I@PPQn}4ad+;=RoJR&N; z!dv29?S0)_?kO%V_YEERmVZcbSxLppWmU_4pQc3xM$h$XpyDc8yjHU7f@lGCLuxAJA_nwFYI_Wk*sEe7{<&Uwyr&j0++IahBi zYhLPHO!PJzeh%|Dgumo|s{Q}t_ZzdCY8=+$X7u7!Ov4*E3!~$VnU8ZZ9>2%ocn)K+ z4>h!phy8IN`iyZ*DuompGBE~AQ7=^BKztsPa1JWK2ISXl<|6@j*m@YVs2@R%bJ;$> zY4r{;W&!OfScQ$4!~EtLg&8#5LPc23tO1;k<8V0!aSzhP{D7MHXJiHD29gAG7ZWjx zhZ-jpqcMO{I0_Y59;#m{rZB&$rog*qu6?jV52&w2y|B&Nib{Pu25~=5!t+r}E~hXV9VA=kDCXc#s0o$-T!=yiItJBVjtX!xYK1je zfHP3ztV3cmEw;WJ2T|XT3iL!0`PaZ#XwZt2lZ`3CTvY1jVIeL;O}GO!@TaK2J5XED zg$lR_)$cFN!+RLS9A;JjD%952q7LtygUG*5=|?muqIOh@enbUu8nw3s|>ZOlLq(P-jfsOMu* zFHS(Mu+BOkHNi5}`;Dmn>ro2~SzAyU-ipNTn4J_9Kqu;Y9kZT8O?(yW@g8za%zW~v z6}^p*<3`j3T{soH@hMCk>`qjJIzuyYB)*RdumhuX|GOw;MsVtph|INgH+2zw+hkJD z#1^bXW#l53V;^eECJuE2t3`gz>wGA{_fTiy6I6iPQCqbObyoK2IrE#ZDKNL`M&>rZ z;KK<}hi)RX>MT^D0(#EYm!Y(KE~SVBR* zfeN4jm7-19%m7<)J@rCnufjv975KP1N@)U)$5K=PD^Qv2M1IXlK9rH$sEkBsxo0Il zi~K8+bQ;K}$wzgpw$4VScqwWHYf$}~ZGA7Qf0wPFvd=G}GV{PbkI8ng-4Im9i*Yu7 zl})XP!k`hZQ&FdO73Sl5d;$-k0=g*b&CJMei-?v8)yt_Zv&{akd4~1S5Sd0 zv93bCT1*HZ-v2!mbdQg?4dxCOQ;*Me1F1ySYfuxcLuKScjK!@OgRRKoOgkz=7f|nA zLe8s6d(0iL47pfl3J%w4SU`buVm8O6Pv z7h)XsmB_Ezz=ys`51<0-vGud)1ZlWJK`Zld9Vjs^4L( z#M7t&2an;~5d-)lE=FzDw@4S$gJba@)PnQJl7FqJjMiOPj+)>K@~vt9#cUkP(Qm^^ zs6%!Wr(rrLSN-cT0au~k3t=+uMXmTKCgKUyLNB4t&P|7cBD{}Esi(-@`%EmQJ{~o| z3iRMRn1zmQ--#OVb5sTn;uP${B#h+>Y~(9Efbr@#&dq2i>Uq{l3fi0BP!n83{Tpx{ zwYR 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' %}
+