diff --git a/amt/api/forms/algorithm.py b/amt/api/forms/algorithm.py index dc08d8b1..e4eb925f 100644 --- a/amt/api/forms/algorithm.py +++ b/amt/api/forms/algorithm.py @@ -6,7 +6,11 @@ async def get_algorithm_form( - id: str, translations: NullTranslations, organizations_service: OrganizationsService, user_id: str | UUID | None + id: str, + translations: NullTranslations, + organizations_service: OrganizationsService, + user_id: str | UUID | None, + organization_id: int | None, ) -> WebForm: _ = translations.gettext @@ -28,7 +32,7 @@ async def get_algorithm_form( WebFormOption(value=str(organization.id), display_value=organization.name) for organization in my_organizations ], - default_value="", + default_value=str(organization_id), group="1", ), ] diff --git a/amt/api/routes/algorithms.py b/amt/api/routes/algorithms.py index 3af0ee99..e2a59211 100644 --- a/amt/api/routes/algorithms.py +++ b/amt/api/routes/algorithms.py @@ -39,33 +39,9 @@ async def get_root( ) -> HTMLResponse: filters, drop_filters, localized_filters, sort_by = get_filters_and_sort_by(request) - amount_algorithm_systems: int = 0 - if display_type == "LIFECYCLE": - algorithms: dict[str, list[Algorithm]] = {} - - # When the lifecycle filter is active, only show these algorithms - if "lifecycle" in filters: - for lifecycle in Lifecycles: - algorithms[lifecycle.name] = [] - algorithms[filters["lifecycle"]] = await algorithms_service.paginate( - skip=skip, limit=limit, search=search, filters=filters, sort=sort_by - ) - amount_algorithm_systems += len(algorithms[filters["lifecycle"]]) - else: - for lifecycle in Lifecycles: - filters["lifecycle"] = lifecycle.name - algorithms[lifecycle.name] = await algorithms_service.paginate( - skip=skip, limit=limit, search=search, filters=filters, sort=sort_by - ) - amount_algorithm_systems += len(algorithms[lifecycle.name]) - else: - algorithms = await algorithms_service.paginate( - skip=skip, limit=limit, search=search, filters=filters, sort=sort_by - ) # pyright: ignore [reportAssignmentType] - # todo: the lifecycle has to be 'localized', maybe for display 'Algorithm' should become a different object - for algorithm in algorithms: - algorithm.lifecycle = get_localized_lifecycle(algorithm.lifecycle, request) # pyright: ignore [reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownArgumentType] - amount_algorithm_systems += len(algorithms) + algorithms, amount_algorithm_systems = await get_algorithms( + algorithms_service, display_type, filters, limit, request, search, skip, sort_by + ) next = skip + limit sub_menu_items = resolve_navigation_items([Navigation.ALGORITHMS_OVERVIEW], request) # pyright: ignore [reportUnusedVariable] # noqa @@ -86,6 +62,7 @@ async def get_root( "filters": localized_filters, "sort_by": sort_by, "display_type": display_type, + "base_href": "/algorithms/", } if request.state.htmx and drop_filters: @@ -96,11 +73,52 @@ async def get_root( return templates.TemplateResponse(request, "algorithms/index.html.j2", context) +async def get_algorithms( + algorithms_service: AlgorithmsService, + display_type: str, + filters: dict[str, str], + limit: int, + request: Request, + search: str, + skip: int, + sort_by: dict[str, str], +) -> tuple[dict[str, list[Algorithm]], int | Any]: + amount_algorithm_systems: int = 0 + if display_type == "LIFECYCLE": + algorithms: dict[str, list[Algorithm]] = {} + + # When the lifecycle filter is active, only show these algorithms + if "lifecycle" in filters: + for lifecycle in Lifecycles: + algorithms[lifecycle.name] = [] + algorithms[filters["lifecycle"]] = await algorithms_service.paginate( + skip=skip, limit=limit, search=search, filters=filters, sort=sort_by + ) + amount_algorithm_systems += len(algorithms[filters["lifecycle"]]) + else: + for lifecycle in Lifecycles: + filters["lifecycle"] = lifecycle.name + algorithms[lifecycle.name] = await algorithms_service.paginate( + skip=skip, limit=limit, search=search, filters=filters, sort=sort_by + ) + amount_algorithm_systems += len(algorithms[lifecycle.name]) + else: + algorithms = await algorithms_service.paginate( + skip=skip, limit=limit, search=search, filters=filters, sort=sort_by + ) # pyright: ignore [reportAssignmentType] + # todo: the lifecycle has to be 'localized', maybe for display 'Algorithm' should become a different object + for algorithm in algorithms: + algorithm.lifecycle = get_localized_lifecycle(algorithm.lifecycle, request) # pyright: ignore [reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownArgumentType] + amount_algorithm_systems += len(algorithms) + return algorithms, amount_algorithm_systems + + @router.get("/new") async def get_new( request: Request, instrument_service: Annotated[InstrumentsService, Depends(create_instrument_service)], organizations_service: Annotated[OrganizationsService, Depends(OrganizationsService)], + organization_id: int = Query(None), ) -> HTMLResponse: sub_menu_items = resolve_navigation_items([Navigation.ALGORITHMS_OVERVIEW], request) # pyright: ignore [reportUnusedVariable] # noqa breadcrumbs = resolve_base_navigation_items([Navigation.ALGORITHMS_ROOT, Navigation.ALGORITHM_NEW], request) @@ -116,6 +134,7 @@ async def get_new( translations=get_current_translation(request), organizations_service=organizations_service, user_id=user["sub"] if user else None, + organization_id=organization_id, ) template_files = get_template_files() diff --git a/amt/api/routes/organizations.py b/amt/api/routes/organizations.py index a474d59b..533a358b 100644 --- a/amt/api/routes/organizations.py +++ b/amt/api/routes/organizations.py @@ -9,6 +9,8 @@ from amt.api.deps import templates from amt.api.forms.organization import get_organization_form +from amt.api.group_by_category import get_localized_group_by_categories +from amt.api.lifecycles import get_localized_lifecycles from amt.api.navigation import ( BaseNavigationItem, Navigation, @@ -17,7 +19,9 @@ resolve_navigation_items, ) from amt.api.organization_filter_options import get_localized_organization_filters +from amt.api.risk_group import get_localized_risk_groups from amt.api.routes.algorithm import UpdateFieldModel, set_path +from amt.api.routes.algorithms import get_algorithms from amt.api.routes.shared import get_filters_and_sort_by from amt.core.authorization import get_user from amt.core.exceptions import AMTAuthorizationError, AMTNotFound, AMTRepositoryError @@ -26,6 +30,7 @@ from amt.repositories.organizations import OrganizationsRepository from amt.repositories.users import UsersRepository from amt.schema.organization import OrganizationBase, OrganizationNew, OrganizationSlug, OrganizationUsers +from amt.services.algorithms import AlgorithmsService from amt.services.organizations import OrganizationsService router = APIRouter() @@ -145,27 +150,34 @@ async def get_by_slug( slug: str, organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], ) -> HTMLResponse: + organization = await get_organization_or_error(organizations_repository, request, slug) + breadcrumbs = resolve_base_navigation_items( + [ + Navigation.ORGANIZATIONS_ROOT, + BaseNavigationItem(custom_display_text=organization.name, url="/organizations/{organization_slug}"), + ], + request, + ) + + tab_items = get_organization_tabs(request, organization_slug=slug) + context = { + "base_href": f"/organizations/{ slug }", + "organization": organization, + "tab_items": tab_items, + "breadcrumbs": breadcrumbs, + } + return templates.TemplateResponse(request, "organizations/home.html.j2", context) + + +async def get_organization_or_error( + organizations_repository: OrganizationsRepository, request: Request, slug: str +) -> Organization: try: organization = await organizations_repository.find_by_slug(slug) request.state.path_variables = {"organization_slug": organization.slug} - breadcrumbs = resolve_base_navigation_items( - [ - Navigation.ORGANIZATIONS_ROOT, - BaseNavigationItem(custom_display_text=organization.name, url="/organizations/{organization_slug}"), - ], - request, - ) - - tab_items = get_organization_tabs(request, organization_slug=slug) - context = { - "base_href": f"/organizations/{ slug }", - "organization": organization, - "tab_items": tab_items, - "breadcrumbs": breadcrumbs, - } - return templates.TemplateResponse(request, "organizations/home.html.j2", context) except AMTRepositoryError as e: raise AMTNotFound from e + return organization @router.get("/{slug}/edit/{path:path}") @@ -177,7 +189,7 @@ async def get_organization_edit( edit_type: str, ) -> HTMLResponse: context: dict[str, Any] = {"base_href": f"/organizations/{ slug }"} - organization = await organizations_repository.find_by_slug(slug) + organization = await get_organization_or_error(organizations_repository, request, slug) context.update({"path": path.replace("/", "."), "edit_type": edit_type, "object": organization}) return templates.TemplateResponse(request, "parts/edit_cell.html.j2", context) @@ -195,7 +207,7 @@ async def get_organization_cancel( "path": path.replace("/", "."), "edit_type": edit_type, } - organization = await organizations_repository.find_by_slug(slug) + organization = await get_organization_or_error(organizations_repository, request, slug) context.update({"object": organization}) return templates.TemplateResponse(request, "parts/view_cell.html.j2", context) @@ -214,7 +226,7 @@ async def get_organization_update( "path": path.replace("/", "."), "edit_type": edit_type, } - organization = await organizations_repository.find_by_slug(slug) + organization = await get_organization_or_error(organizations_repository, request, slug) context.update({"object": organization}) redirect_to: str | None = None @@ -244,10 +256,62 @@ async def get_organization_update( @router.get("/{slug}/algorithms") -async def get_algorithms( +async def show_algorithms( request: Request, + algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], + organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], + 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: - return templates.TemplateResponse(request, "pages/under_construction.html.j2", {}) + organization = await get_organization_or_error(organizations_repository, request, slug) + filters, drop_filters, localized_filters, sort_by = get_filters_and_sort_by(request) + + filters["organization-id"] = str(organization.id) + algorithms, amount_algorithm_systems = await get_algorithms( + algorithms_service, display_type, filters, limit, request, search, skip, sort_by + ) + next = skip + limit + + tab_items = get_organization_tabs(request, organization_slug=slug) + + breadcrumbs = resolve_base_navigation_items( + [ + Navigation.ORGANIZATIONS_ROOT, + BaseNavigationItem(custom_display_text=organization.name, url="/organizations/{organization_slug}"), + Navigation.ORGANIZATIONS_ALGORITHMS, + ], + request, + ) + + context: dict[str, Any] = { + "breadcrumbs": breadcrumbs, + "tab_items": tab_items, + "sub_menu_items": {}, + "algorithms": algorithms, + "amount_algorithm_systems": amount_algorithm_systems, + "next": next, + "limit": limit, + "start": skip, + "search": search, + "lifecycles": get_localized_lifecycles(request), + "risk_groups": get_localized_risk_groups(request), + "group_by_categories": get_localized_group_by_categories(request), + "filters": localized_filters, + "sort_by": sort_by, + "display_type": display_type, + "base_href": f"/organizations/{slug}/algorithms", + "organization_id": organization.id, + } + + if request.state.htmx and drop_filters: + return templates.TemplateResponse(request, "parts/algorithm_search.html.j2", context) + elif request.state.htmx: + return templates.TemplateResponse(request, "parts/filter_list.html.j2", context) + else: + return templates.TemplateResponse(request, "organizations/algorithms.html.j2", context) @router.delete("/{slug}/members/{user_id}") @@ -259,7 +323,7 @@ async def remove_member( users_repository: Annotated[UsersRepository, Depends(UsersRepository)], ) -> HTMLResponse: # TODO (Robbert): add authorization and check if user and organization exist? - organization = await organizations_repository.find_by_slug(slug) + organization = await get_organization_or_error(organizations_repository, request, slug) user: User | None = await users_repository.find_by_id(user_id) if user: await organizations_repository.remove_user(organization, user) @@ -281,10 +345,11 @@ async def get_members_form( async def add_new_members( request: Request, slug: str, + organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], organizations_service: Annotated[OrganizationsService, Depends(OrganizationsService)], organization_users: OrganizationUsers, ) -> HTMLResponse: - organization = await organizations_service.find_by_slug(slug) + organization = await get_organization_or_error(organizations_repository, request, slug) await organizations_service.add_users(organization, organization_users.user_ids) return templates.Redirect(request, f"/organizations/{slug}/members") @@ -299,10 +364,9 @@ async def get_members( limit: int = Query(5000, ge=1), # todo: fix infinite scroll search: str = Query(""), ) -> HTMLResponse: + organization = await get_organization_or_error(organizations_repository, request, slug) filters, drop_filters, localized_filters, sort_by = get_filters_and_sort_by(request) - organization = await organizations_repository.find_by_slug(slug) tab_items = get_organization_tabs(request, organization_slug=slug) - request.state.path_variables = {"organization_slug": organization.slug} breadcrumbs = resolve_base_navigation_items( [ Navigation.ORGANIZATIONS_ROOT, diff --git a/amt/core/exception_handlers.py b/amt/core/exception_handlers.py index 3a06f261..c7777f49 100644 --- a/amt/core/exception_handlers.py +++ b/amt/core/exception_handlers.py @@ -21,6 +21,7 @@ "missing": _("Field required"), "value_error": _("Field required"), "string_pattern_mismatch": _("String should match pattern '{pattern}'"), + "int_parsing": _("Input should be a valid integer."), } @@ -78,6 +79,7 @@ async def general_exception_handler(request: Request, exc: Exception) -> HTMLRes request, template_name, {"message": message}, status_code=status_code, headers=response_headers ) except Exception: + logger.exception("Can not display error template") response = templates.TemplateResponse( request, fallback_template_name, diff --git a/amt/locale/base.pot b/amt/locale/base.pot index 36de6717..8ba12e3d 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -48,7 +48,7 @@ msgstr "" #: amt/api/group_by_category.py:15 #: amt/site/templates/algorithms/details_info.html.j2:28 #: amt/site/templates/algorithms/new.html.j2:41 -#: amt/site/templates/parts/algorithm_search.html.j2:47 +#: amt/site/templates/parts/algorithm_search.html.j2:48 #: amt/site/templates/parts/filter_list.html.j2:71 msgid "Lifecycle" msgstr "" @@ -126,7 +126,7 @@ msgstr "" msgid "Details" msgstr "" -#: amt/api/navigation.py:56 +#: amt/api/navigation.py:56 amt/site/templates/organizations/home.html.j2:8 msgid "Info" msgstr "" @@ -155,7 +155,7 @@ msgstr "" msgid "Organizations" msgstr "" -#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:32 +#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:33 #: amt/site/templates/organizations/parts/members_results.html.j2:6 #: amt/site/templates/organizations/parts/overview_results.html.j2:132 msgid "Members" @@ -189,11 +189,11 @@ msgstr "" msgid "niet van toepassing" msgstr "" -#: amt/api/forms/algorithm.py:19 +#: amt/api/forms/algorithm.py:23 msgid "Select organization" msgstr "" -#: amt/api/forms/algorithm.py:25 +#: amt/api/forms/algorithm.py:29 #: amt/site/templates/algorithms/details_info.html.j2:12 msgid "Organization" msgstr "" @@ -243,7 +243,7 @@ msgstr "" #: amt/api/forms/organization.py:23 #: amt/site/templates/algorithms/details_info.html.j2:8 #: amt/site/templates/auth/profile.html.j2:34 -#: amt/site/templates/organizations/home.html.j2:12 +#: amt/site/templates/organizations/home.html.j2:13 #: amt/site/templates/organizations/parts/members_results.html.j2:118 msgid "Name" msgstr "" @@ -257,7 +257,7 @@ msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" #: amt/api/forms/organization.py:33 -#: amt/site/templates/organizations/home.html.j2:16 +#: amt/site/templates/organizations/home.html.j2:17 msgid "Slug" msgstr "" @@ -291,6 +291,10 @@ msgstr "" msgid "String should match pattern '{pattern}'" msgstr "" +#: amt/core/exception_handlers.py:24 +msgid "Input should be a valid integer." +msgstr "" + #: amt/core/exceptions.py:20 msgid "" "An error occurred while configuring the options for '{field}'. Please " @@ -504,12 +508,12 @@ msgstr "" msgid "Read more on the algoritmekader" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:63 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:64 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:67 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:68 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" @@ -631,6 +635,7 @@ msgid "Login" msgstr "" #: amt/site/templates/errors/Exception.html.j2:5 +#: amt/site/templates/errors/RequestValidationError_400.html.j2:5 msgid "An error occurred" msgstr "" @@ -647,7 +652,7 @@ msgstr "" msgid "There is one error:" msgstr "" -#: amt/site/templates/layouts/base.html.j2:11 +#: amt/site/templates/layouts/base.html.j2:1 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" @@ -695,15 +700,15 @@ msgstr "" msgid "Unknown" msgstr "" -#: amt/site/templates/organizations/home.html.j2:20 +#: amt/site/templates/organizations/home.html.j2:21 msgid "Created at" msgstr "" -#: amt/site/templates/organizations/home.html.j2:24 +#: amt/site/templates/organizations/home.html.j2:25 msgid "Created by" msgstr "" -#: amt/site/templates/organizations/home.html.j2:28 +#: amt/site/templates/organizations/home.html.j2:29 msgid "Modified at" msgstr "" @@ -722,7 +727,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:24 +#: amt/site/templates/parts/algorithm_search.html.j2:25 msgid "Search" msgstr "" @@ -866,31 +871,31 @@ msgstr "" msgid "This page is yet to be build." msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:14 +#: amt/site/templates/parts/algorithm_search.html.j2:15 msgid "New algorithm" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:31 +#: amt/site/templates/parts/algorithm_search.html.j2:32 msgid "Find algorithm..." msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:52 +#: amt/site/templates/parts/algorithm_search.html.j2:53 msgid "Select lifecycle" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:61 +#: amt/site/templates/parts/algorithm_search.html.j2:62 msgid "Category" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:66 +#: amt/site/templates/parts/algorithm_search.html.j2:67 msgid "Select risk group" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:82 +#: amt/site/templates/parts/algorithm_search.html.j2:83 msgid "Group by" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:92 +#: amt/site/templates/parts/algorithm_search.html.j2:88 msgid "Select group by" msgstr "" @@ -915,6 +920,10 @@ msgid "" " algorithms." msgstr "" +#: amt/site/templates/parts/filter_list.html.j2:116 +msgid "Does not exist" +msgstr "" + #: amt/site/templates/parts/header.html.j2:7 msgid "Beta Version" msgstr "" diff --git a/amt/locale/en_US/LC_MESSAGES/messages.mo b/amt/locale/en_US/LC_MESSAGES/messages.mo index 738f59d7..9948b9f6 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 64b4dda5..bee1995d 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -49,7 +49,7 @@ msgstr "" #: amt/api/group_by_category.py:15 #: amt/site/templates/algorithms/details_info.html.j2:28 #: amt/site/templates/algorithms/new.html.j2:41 -#: amt/site/templates/parts/algorithm_search.html.j2:47 +#: amt/site/templates/parts/algorithm_search.html.j2:48 #: amt/site/templates/parts/filter_list.html.j2:71 msgid "Lifecycle" msgstr "" @@ -127,7 +127,7 @@ msgstr "" msgid "Details" msgstr "" -#: amt/api/navigation.py:56 +#: amt/api/navigation.py:56 amt/site/templates/organizations/home.html.j2:8 msgid "Info" msgstr "" @@ -156,7 +156,7 @@ msgstr "" msgid "Organizations" msgstr "" -#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:32 +#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:33 #: amt/site/templates/organizations/parts/members_results.html.j2:6 #: amt/site/templates/organizations/parts/overview_results.html.j2:132 msgid "Members" @@ -190,11 +190,11 @@ msgstr "Exception of application" msgid "niet van toepassing" msgstr "Not applicable" -#: amt/api/forms/algorithm.py:19 +#: amt/api/forms/algorithm.py:23 msgid "Select organization" msgstr "" -#: amt/api/forms/algorithm.py:25 +#: amt/api/forms/algorithm.py:29 #: amt/site/templates/algorithms/details_info.html.j2:12 msgid "Organization" msgstr "" @@ -244,7 +244,7 @@ msgstr "" #: amt/api/forms/organization.py:23 #: amt/site/templates/algorithms/details_info.html.j2:8 #: amt/site/templates/auth/profile.html.j2:34 -#: amt/site/templates/organizations/home.html.j2:12 +#: amt/site/templates/organizations/home.html.j2:13 #: amt/site/templates/organizations/parts/members_results.html.j2:118 msgid "Name" msgstr "" @@ -258,7 +258,7 @@ msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" #: amt/api/forms/organization.py:33 -#: amt/site/templates/organizations/home.html.j2:16 +#: amt/site/templates/organizations/home.html.j2:17 msgid "Slug" msgstr "" @@ -292,6 +292,10 @@ msgstr "" msgid "String should match pattern '{pattern}'" msgstr "" +#: amt/core/exception_handlers.py:24 +msgid "Input should be a valid integer." +msgstr "" + #: amt/core/exceptions.py:20 msgid "" "An error occurred while configuring the options for '{field}'. Please " @@ -505,12 +509,12 @@ msgstr "" msgid "Read more on the algoritmekader" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:63 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:64 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:67 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:68 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" @@ -632,6 +636,7 @@ msgid "Login" msgstr "" #: amt/site/templates/errors/Exception.html.j2:5 +#: amt/site/templates/errors/RequestValidationError_400.html.j2:5 msgid "An error occurred" msgstr "" @@ -648,7 +653,7 @@ msgstr "" msgid "There is one error:" msgstr "" -#: amt/site/templates/layouts/base.html.j2:11 +#: amt/site/templates/layouts/base.html.j2:1 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" @@ -696,15 +701,15 @@ msgstr "" msgid "Unknown" msgstr "" -#: amt/site/templates/organizations/home.html.j2:20 +#: amt/site/templates/organizations/home.html.j2:21 msgid "Created at" msgstr "" -#: amt/site/templates/organizations/home.html.j2:24 +#: amt/site/templates/organizations/home.html.j2:25 msgid "Created by" msgstr "" -#: amt/site/templates/organizations/home.html.j2:28 +#: amt/site/templates/organizations/home.html.j2:29 msgid "Modified at" msgstr "" @@ -723,7 +728,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:24 +#: amt/site/templates/parts/algorithm_search.html.j2:25 msgid "Search" msgstr "" @@ -867,31 +872,31 @@ msgstr "" msgid "This page is yet to be build." msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:14 +#: amt/site/templates/parts/algorithm_search.html.j2:15 msgid "New algorithm" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:31 +#: amt/site/templates/parts/algorithm_search.html.j2:32 msgid "Find algorithm..." msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:52 +#: amt/site/templates/parts/algorithm_search.html.j2:53 msgid "Select lifecycle" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:61 +#: amt/site/templates/parts/algorithm_search.html.j2:62 msgid "Category" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:66 +#: amt/site/templates/parts/algorithm_search.html.j2:67 msgid "Select risk group" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:82 +#: amt/site/templates/parts/algorithm_search.html.j2:83 msgid "Group by" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:92 +#: amt/site/templates/parts/algorithm_search.html.j2:88 msgid "Select group by" msgstr "" @@ -916,6 +921,10 @@ msgid "" " algorithms." msgstr "" +#: amt/site/templates/parts/filter_list.html.j2:116 +msgid "Does not exist" +msgstr "" + #: amt/site/templates/parts/header.html.j2:7 msgid "Beta Version" msgstr "" diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.mo b/amt/locale/nl_NL/LC_MESSAGES/messages.mo index 87b5523a..b7171443 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 9a794fa7..f4210748 100644 --- a/amt/locale/nl_NL/LC_MESSAGES/messages.po +++ b/amt/locale/nl_NL/LC_MESSAGES/messages.po @@ -51,7 +51,7 @@ msgstr "Rol" #: amt/api/group_by_category.py:15 #: amt/site/templates/algorithms/details_info.html.j2:28 #: amt/site/templates/algorithms/new.html.j2:41 -#: amt/site/templates/parts/algorithm_search.html.j2:47 +#: amt/site/templates/parts/algorithm_search.html.j2:48 #: amt/site/templates/parts/filter_list.html.j2:71 msgid "Lifecycle" msgstr "Levenscyclus" @@ -129,7 +129,7 @@ msgstr "Model kaart" msgid "Details" msgstr "Details" -#: amt/api/navigation.py:56 +#: amt/api/navigation.py:56 amt/site/templates/organizations/home.html.j2:8 msgid "Info" msgstr "Info" @@ -158,7 +158,7 @@ msgstr "Instrumenten" msgid "Organizations" msgstr "Organisaties" -#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:32 +#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:33 #: amt/site/templates/organizations/parts/members_results.html.j2:6 #: amt/site/templates/organizations/parts/overview_results.html.j2:132 msgid "Members" @@ -192,11 +192,11 @@ msgstr "Uitzondering van toepassing" msgid "niet van toepassing" msgstr "Niet van toepassing" -#: amt/api/forms/algorithm.py:19 +#: amt/api/forms/algorithm.py:23 msgid "Select organization" msgstr "Selecteer organisatie" -#: amt/api/forms/algorithm.py:25 +#: amt/api/forms/algorithm.py:29 #: amt/site/templates/algorithms/details_info.html.j2:12 msgid "Organization" msgstr "Organisatie" @@ -248,7 +248,7 @@ msgstr "Voeg link naar bestanden toe" #: amt/api/forms/organization.py:23 #: amt/site/templates/algorithms/details_info.html.j2:8 #: amt/site/templates/auth/profile.html.j2:34 -#: amt/site/templates/organizations/home.html.j2:12 +#: amt/site/templates/organizations/home.html.j2:13 #: amt/site/templates/organizations/parts/members_results.html.j2:118 msgid "Name" msgstr "Naam" @@ -264,7 +264,7 @@ msgstr "" "organisatie-naam" #: amt/api/forms/organization.py:33 -#: amt/site/templates/organizations/home.html.j2:16 +#: amt/site/templates/organizations/home.html.j2:17 msgid "Slug" msgstr "Slug" @@ -298,6 +298,10 @@ msgstr "Veld verplicht" msgid "String should match pattern '{pattern}'" msgstr "De waarde moet voldoen aan het patroon '{pattern}'" +#: amt/core/exception_handlers.py:24 +msgid "Input should be a valid integer." +msgstr "Invoer moet een valide getal zijn." + #: amt/core/exceptions.py:20 msgid "" "An error occurred while configuring the options for '{field}'. Please " @@ -523,12 +527,12 @@ msgstr "Referenties" msgid "Read more on the algoritmekader" msgstr "Lees meer op het algoritmekader" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:63 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:64 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "Opslaan" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:67 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:68 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" @@ -655,6 +659,7 @@ msgid "Login" msgstr "Inloggen" #: amt/site/templates/errors/Exception.html.j2:5 +#: amt/site/templates/errors/RequestValidationError_400.html.j2:5 msgid "An error occurred" msgstr "Er is een fout opgetreden" @@ -671,7 +676,7 @@ msgstr "Er zijn enkele fouten" msgid "There is one error:" msgstr "Er is één fout:" -#: amt/site/templates/layouts/base.html.j2:11 +#: amt/site/templates/layouts/base.html.j2:1 msgid "Algorithmic Management Toolkit (AMT)" msgstr "Algoritme Management Toolkit" @@ -719,15 +724,15 @@ msgstr "Beoordelen" msgid "Unknown" msgstr "Onbekend" -#: amt/site/templates/organizations/home.html.j2:20 +#: amt/site/templates/organizations/home.html.j2:21 msgid "Created at" msgstr "Aangemaakt op" -#: amt/site/templates/organizations/home.html.j2:24 +#: amt/site/templates/organizations/home.html.j2:25 msgid "Created by" msgstr "Aangemaakt door" -#: amt/site/templates/organizations/home.html.j2:28 +#: amt/site/templates/organizations/home.html.j2:29 msgid "Modified at" msgstr "Bijgewerkt op" @@ -746,7 +751,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:24 +#: amt/site/templates/parts/algorithm_search.html.j2:25 msgid "Search" msgstr "Zoek" @@ -897,31 +902,31 @@ msgstr "In ontwikkeling" msgid "This page is yet to be build." msgstr "Deze pagina is nog niet klaar." -#: amt/site/templates/parts/algorithm_search.html.j2:14 +#: amt/site/templates/parts/algorithm_search.html.j2:15 msgid "New algorithm" msgstr "Nieuw algoritme" -#: amt/site/templates/parts/algorithm_search.html.j2:31 +#: amt/site/templates/parts/algorithm_search.html.j2:32 msgid "Find algorithm..." msgstr "Vind algoritme..." -#: amt/site/templates/parts/algorithm_search.html.j2:52 +#: amt/site/templates/parts/algorithm_search.html.j2:53 msgid "Select lifecycle" msgstr "Selecteer levenscyclus" -#: amt/site/templates/parts/algorithm_search.html.j2:61 +#: amt/site/templates/parts/algorithm_search.html.j2:62 msgid "Category" msgstr "Categorie" -#: amt/site/templates/parts/algorithm_search.html.j2:66 +#: amt/site/templates/parts/algorithm_search.html.j2:67 msgid "Select risk group" msgstr "Selecteer groepering" -#: amt/site/templates/parts/algorithm_search.html.j2:82 +#: amt/site/templates/parts/algorithm_search.html.j2:83 msgid "Group by" msgstr "Groeperen op" -#: amt/site/templates/parts/algorithm_search.html.j2:92 +#: amt/site/templates/parts/algorithm_search.html.j2:88 msgid "Select group by" msgstr "Selecteer groepering" @@ -954,6 +959,10 @@ msgstr "" "Er zijn geen Algoritmes die overeen komen met de geselecteerde filters. " "Pas de filters aan of verwijder ze om meer algoritmes te zien." +#: amt/site/templates/parts/filter_list.html.j2:116 +msgid "Does not exist" +msgstr "Bestaat niet" + #: amt/site/templates/parts/header.html.j2:7 msgid "Beta Version" msgstr "Bètaversie" diff --git a/amt/repositories/algorithms.py b/amt/repositories/algorithms.py index c7e3a48c..e4edc874 100644 --- a/amt/repositories/algorithms.py +++ b/amt/repositories/algorithms.py @@ -90,6 +90,8 @@ async def paginate( # noqa Algorithm.system_card_json["ai_act_profile"]["risk_group"].as_string() == RiskGroup[value].value ) + case "organization-id": + statement = statement.filter(Algorithm.organization_id == int(value)) case _: raise TypeError(f"Unknown filter type with key: {key}") # noqa if sort: diff --git a/amt/site/static/ts/amt.ts b/amt/site/static/ts/amt.ts index 429ed072..c6fa62be 100644 --- a/amt/site/static/ts/amt.ts +++ b/amt/site/static/ts/amt.ts @@ -426,3 +426,5 @@ export function getFiles(element: HTMLInputElement, target_id: string) { } } } + +// for debugging htmx use -> htmx.logAll(); diff --git a/amt/site/templates/errors/RequestValidationError_400.html.j2 b/amt/site/templates/errors/RequestValidationError_400.html.j2 new file mode 100644 index 00000000..d19aa57b --- /dev/null +++ b/amt/site/templates/errors/RequestValidationError_400.html.j2 @@ -0,0 +1,9 @@ +{% extends 'layouts/base.html.j2' %} +{% block content %} +
+
+

{% trans %}An error occurred{% endtrans %}

+ {% include 'errors/_RequestValidationError_400.html.j2' %} +
+
+{% endblock %} diff --git a/amt/site/templates/errors/_RequestValidationError_400.html.j2 b/amt/site/templates/errors/_RequestValidationError_400.html.j2 index 9a54ab2d..63e58e0a 100644 --- a/amt/site/templates/errors/_RequestValidationError_400.html.j2 +++ b/amt/site/templates/errors/_RequestValidationError_400.html.j2 @@ -14,13 +14,15 @@ {% endfor %} -{% for msg in message %} -
- - {{ msg['msg'] }} -
-{% endfor %} +{% if request.state.htmx is defined and request.state.htmx %} + {% for msg in message %} +
+ + {{ msg['msg'] }} +
+ {% endfor %} +{% endif %} diff --git a/amt/site/templates/organizations/algorithms.html.j2 b/amt/site/templates/organizations/algorithms.html.j2 new file mode 100644 index 00000000..454bc8e9 --- /dev/null +++ b/amt/site/templates/organizations/algorithms.html.j2 @@ -0,0 +1,10 @@ +{% import "macros/form_macros.html.j2" as macros with context %} +{% import "macros/tabs.html.j2" as tabs with context %} +{% extends "layouts/base.html.j2" %} +{% block content %} +
+ {{ tabs.show_tabs(tab_items) }} +
{% include 'parts/algorithm_search.html.j2' %}
+
+{% endblock content %} diff --git a/amt/site/templates/organizations/home.html.j2 b/amt/site/templates/organizations/home.html.j2 index 0b6f6dc5..c3b45ba5 100644 --- a/amt/site/templates/organizations/home.html.j2 +++ b/amt/site/templates/organizations/home.html.j2 @@ -5,6 +5,7 @@ {% block content %}
{{ tabs.show_tabs(tab_items) }} +

{% trans %}Info{% endtrans %}

diff --git a/amt/site/templates/parts/algorithm_search.html.j2 b/amt/site/templates/parts/algorithm_search.html.j2 index 79ec33f0..e121ab57 100644 --- a/amt/site/templates/parts/algorithm_search.html.j2 +++ b/amt/site/templates/parts/algorithm_search.html.j2 @@ -1,7 +1,7 @@
@@ -9,13 +9,14 @@

{% trans %}Algorithms{% endtrans %}

- +
-
+
@@ -30,7 +31,7 @@ value="{{ search }}" placeholder="{% trans %}Find algorithm...{% endtrans %}" name="search" - hx-get="/algorithms/?skip=0" + hx-get="{{ base_href }}?skip=0" hx-trigger="input changed delay:500ms, search" autocomplete="off" onfocus="this.value=''" /> @@ -38,7 +39,7 @@ role="img" aria-label="Kruis" hx-trigger="click" - hx-get="/algorithms/?skip=0" + hx-get="{{ base_href }}?skip=0" hx-swap="innerHTML" onclick="document.getElementById('algorithm-search-input').value = ''">
@@ -81,21 +82,15 @@ justify-content: flex-end">
{% trans %}Group by{% endtrans %}
- - - +
diff --git a/amt/site/templates/parts/filter_list.html.j2 b/amt/site/templates/parts/filter_list.html.j2 index 51756a39..8c20fee4 100644 --- a/amt/site/templates/parts/filter_list.html.j2 +++ b/amt/site/templates/parts/filter_list.html.j2 @@ -26,7 +26,7 @@ @@ -65,15 +65,15 @@
@@ -111,9 +111,9 @@
{% for lifecycle in lifecycles %}{{ render.row(algorithms[lifecycle.value], lifecycle) }}{% endfor %}
- {% else %} - Does not exist - {% endif %} - + + {% else %} + {% trans %}Does not exist{% endtrans %} + {% endif %} {% endif %} {% endif %} diff --git a/amt/site/templates/parts/header.html.j2 b/amt/site/templates/parts/header.html.j2 index 706a5bc6..13f62ae3 100644 --- a/amt/site/templates/parts/header.html.j2 +++ b/amt/site/templates/parts/header.html.j2 @@ -159,7 +159,7 @@ -{% if sub_menu_items is defined %} +{% if sub_menu_items is defined and sub_menu_items|length > 0 %}
diff --git a/tests/api/routes/test_organizations.py b/tests/api/routes/test_organizations.py index 5f80245d..744403ca 100644 --- a/tests/api/routes/test_organizations.py +++ b/tests/api/routes/test_organizations.py @@ -4,7 +4,12 @@ from httpx import AsyncClient from pytest_mock import MockFixture -from tests.constants import default_auth_user, default_user, default_user_without_default_organization +from tests.constants import ( + default_algorithm, + default_auth_user, + default_user, + default_user_without_default_organization, +) from tests.database_test_utils import DatabaseTestUtils @@ -259,3 +264,44 @@ async def test_add_member(client: AsyncClient, mocker: MockFixture, db: Database assert response.status_code == 200 assert response.headers["content-type"] == "text/html; charset=utf-8" assert response.headers["HX-Redirect"] == "/organizations/default-organization/members" + + +@pytest.mark.asyncio +async def test_get_algorithms(client: AsyncClient, mocker: MockFixture, db: DatabaseTestUtils) -> None: + # given + await db.given( + [ + default_user(), + default_algorithm(name="Algorithm1"), + default_algorithm(name="Algorithm2", organization_id=1), + ] + ) + client.cookies["fastapi-csrf-token"] = "1" + + mocker.patch("amt.api.routes.organizations.get_user", return_value=default_auth_user()) + mocker.patch("fastapi_csrf_protect.CsrfProtect.validate_csrf", new_callable=mocker.AsyncMock) + + # when + response = await client.get("/organizations/default-organization/algorithms") + # then + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + assert b"Algorithm1" in response.content + assert b"Algorithm2" in response.content + + # when + response = await client.get("/organizations/default-organization/algorithms?search=Algorithm1") + # then + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + assert b"Algorithm1" in response.content + assert b"Algorithm2" not in response.content + + # when + query = "?skip=5&search=&add-filter-lifecycle=&add-filter-risk-group=VERBODEN_AI&display_type" + response = await client.get("/organizations/default-organization/algorithms" + query) + # then + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + assert b"Algorithm1" not in response.content + assert b"Algorithm2" not in response.content diff --git a/tests/repositories/test_algorithms.py b/tests/repositories/test_algorithms.py index 9de1998e..b4fd1999 100644 --- a/tests/repositories/test_algorithms.py +++ b/tests/repositories/test_algorithms.py @@ -10,6 +10,7 @@ default_algorithm, default_algorithm_with_lifecycle, default_algorithm_with_system_card, + default_organization, default_user, ) from tests.database_test_utils import DatabaseTestUtils @@ -332,3 +333,36 @@ async def test_with_sorting(db: DatabaseTestUtils): skip=0, limit=4, search="", filters={}, sort={"lifecycle": "descending"} ) assert result[0].name == "Algorithm2" + + +@pytest.mark.asyncio +async def test_with_organization_filter(db: DatabaseTestUtils): + await db.given( + [ + default_user(), + default_algorithm(name="Algorithm1"), + default_organization(name="Organization 2", slug="organization-2"), + default_algorithm(name="Algorithm2", organization_id=2), + ] + ) + algorithm_repository = AlgorithmsRepository(db.get_session()) + + result: list[Algorithm] = await algorithm_repository.paginate( + skip=0, limit=4, search="", filters={"organization-id": "1"}, sort={} + ) + + assert len(result) == 1 + assert result[0].name == "Algorithm1" + + result: list[Algorithm] = await algorithm_repository.paginate( + skip=0, limit=4, search="", filters={"organization-id": "2"}, sort={} + ) + + assert len(result) == 1 + assert result[0].name == "Algorithm2" + + result: list[Algorithm] = await algorithm_repository.paginate( + skip=0, limit=4, search="", filters={"organization-id": "99"}, sort={} + ) + + assert len(result) == 0
{% trans %}Algorithm name{% endtrans %} - {{ table_row.sort_button('name', sort_by, "/algorithms/") }} + {{ table_row.sort_button('name', sort_by, base_href) }} {% trans %}Lifecycle{% endtrans %} - {{ table_row.sort_button('lifecycle', sort_by, "/algorithms/") }} + {{ table_row.sort_button('lifecycle', sort_by, base_href) }} {% trans %}Last updated{% endtrans %} - {{ table_row.sort_button('last_update', sort_by, "/algorithms/") }} + {{ table_row.sort_button('last_update', sort_by, base_href) }}