From bbd3b58b1d1f8dce257b1c968af2ff17f919c2d9 Mon Sep 17 00:00:00 2001 From: robbertuittenbroek Date: Wed, 18 Dec 2024 10:29:01 +0100 Subject: [PATCH] Add algorithm overview for organization --- amt/api/forms/algorithm.py | 8 +- amt/api/routes/algorithms.py | 73 ++++++----- amt/api/routes/organizations.py | 114 ++++++++++++++---- amt/core/exception_handlers.py | 2 + amt/locale/base.pot | 43 ++++--- amt/locale/en_US/LC_MESSAGES/messages.mo | Bin 989 -> 989 bytes amt/locale/en_US/LC_MESSAGES/messages.po | 43 ++++--- amt/locale/nl_NL/LC_MESSAGES/messages.mo | Bin 14529 -> 14613 bytes amt/locale/nl_NL/LC_MESSAGES/messages.po | 43 ++++--- amt/repositories/algorithms.py | 2 + amt/site/static/ts/amt.ts | 2 + .../errors/RequestValidationError_400.html.j2 | 9 ++ .../_RequestValidationError_400.html.j2 | 22 ++-- .../organizations/algorithms.html.j2 | 10 ++ amt/site/templates/organizations/home.html.j2 | 1 + .../templates/parts/algorithm_search.html.j2 | 37 +++--- amt/site/templates/parts/filter_list.html.j2 | 16 +-- amt/site/templates/parts/header.html.j2 | 2 +- tests/api/routes/test_organizations.py | 48 +++++++- tests/repositories/test_algorithms.py | 34 ++++++ 20 files changed, 357 insertions(+), 152 deletions(-) create mode 100644 amt/site/templates/errors/RequestValidationError_400.html.j2 create mode 100644 amt/site/templates/organizations/algorithms.html.j2 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 1fc801d2..7ba8b12c 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 "" @@ -231,7 +231,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 "" @@ -245,7 +245,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 "" @@ -279,6 +279,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 " @@ -619,6 +623,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 "" @@ -635,7 +640,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 "" @@ -683,15 +688,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 "" @@ -710,7 +715,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 "" @@ -854,31 +859,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 "" diff --git a/amt/locale/en_US/LC_MESSAGES/messages.mo b/amt/locale/en_US/LC_MESSAGES/messages.mo index a583a5e5c1057389713e496a0da0c677e2922a74..b7cbf3ff1da015d4a8250cd5c54b25f420a3dd97 100644 GIT binary patch delta 20 bcmcc1ewTg2RYrCT1w#WXBg@Tq7#W!WOx6ZR delta 20 bcmcc1ewTg2RYrDm1w%6{6NAln7#W!WOxOlR diff --git a/amt/locale/en_US/LC_MESSAGES/messages.po b/amt/locale/en_US/LC_MESSAGES/messages.po index e939c29b..68c3b694 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 "" @@ -232,7 +232,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 "" @@ -246,7 +246,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 "" @@ -280,6 +280,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 " @@ -620,6 +624,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 "" @@ -636,7 +641,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 "" @@ -684,15 +689,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 "" @@ -711,7 +716,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 "" @@ -855,31 +860,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 "" diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.mo b/amt/locale/nl_NL/LC_MESSAGES/messages.mo index 9be3eaba4cde11403e81794323df1edf1a8b6cfb..e9d76276b011c4a17e1ce2680eae26597161592d 100644 GIT binary patch delta 3306 zcmYM#3rv<(9LMovidcq28SH}n`AEePs43@T zIObs_7NOr5*MulU(NKv#+=X4S3ghu8DzMLyUvr(0IJ{%);jzZ_rS3<~^DwG^ymhv9 z3Fgzj7N5c&F^%<2I;+j5VGb(NBbbcGF$bH_!8m5+8Iy%tU@RZn$+JjO%mP$^OR*cS zM$K1=5%>ls;a&{Gdi1ltIZc5zOcUzCHg(`Xs2wHHD3ejC?~e|q<8+*gTBr(p;$c)K z&myN`F4=kuYFr!YD1Jv*1KTM~z%cTihLcedmZKJ|Kn1wY*0-Yq+Jzc-0GYy6BfqAe zj|{wo+Hr>jFEb9ZCzEdLV-m=JcN(VBpvV{70WJ=pz75sifXc`f9FA?Ml%~?D00*K{ zpNZPgXw*iYw*4U-OnnVHcnCG_Vt4Ydv;LX}-R8TfTN}q}3Md-jOyWKA{G@`0G0Al z*b%3p&UyywxkY#%mSF{!qsHGsjf-Fs)m=XY?I;Z$9ELihS+>2<)UHrI_pGC>A<}~1vEatvysampJD!FV3W}%@=dqIt)YqxW z@d9f__5X#sR9*Ub#-SEWKxHHYcXZ$m;AZNVQ@mI0e7;f&_#i%nAL2-CN+tgaAfd09 zx_L;SDMjtZ4KPpRx5^+rK>EdMT;2 z19l$+&L`d~dNQ>S%@`_s-0|-JrR)cc#2cuc{fUY+hJRf$9u;UWjKToMUj zjIBmJ_d066z36Jd0SY>+I_!w&a3fwoech&|dkYn#B43LNWGgbn>_NS1FWUBITW>{& z_IA|HduMnX?1!&V&&(kIis(2E0X&Pk&21QqCezDAS5(CPP`7^&DuBtT=jWg@muKt6 zsEw>bUD^s%MmC@}uo*S~+nMBFsjIaE&Y)6%9$&(1sDPf&^2UX59Q7@z3BSTA*oZSQ zFvL6iHOL;!I;6|gp)znCwUOKS9=5v_w7@%D-2yy{x`avDysdBos(&ZO;RncT-JC=P z-h$fUZR_9Ije19JZWr{Ub{at4v24`k9EHlHJC1@bM-X+JD^TzFcTp2pp(dz7?YJH% z;wg+rUyd=a@y<_1Em$+$%isxA|7FxsTu05@hWelJD{_QkCUOM-(e|Ze*KLdTMJ6p@ zS>~)LC|OyQ=PV35xlSmzs4&kdEG`S?2TKRn?e5;_3m@)eW{k`lQ5Wj-xX%fd1WTP| fCBZT$7%cXN2c7(2S#FWDx^PMH;JRZ2ebN5`YVlOK delta 3236 zcmYM$drZ}39LMpmc|h@ifPxyS91&DR?0^d7yd{d_eR(6KSZ)GEj&zomoqn2^NXe!G z%W64wq^8yi8(GbJPHX0zEhkgwWz=$N!))0f&EB8=p4J(h*K_&(p6B~~pXc|3hNZ`r zcvrgx8jR0LetYvfDq6MwKV`9IYpFhi^Y8`+@QFCHM{x|6VhL7YGj_!kzWQS(YRKu> z3Flx8mSGPw&nhT%p&^1nti=SZ$L`pK3hWT_V?Xnih;6R^7p7B>j`zpOLiOi4Cp$~A zlJ*yI3jTzd%x|g8RzgEDD$<>pihD61&tM2UFe>+08ft=EzO<4FNK$MDD!>KU4PQWw z7s1ZB7W?8m7=`<>2lLw(6qv)#pl)nc2i`$tCP1T%N2NXmLzs%QaU5!*dhCTCpfY(7 z*$q4D>OZ2cYej9vb@X)MZxklspO}fG$-5$~L`}FD72r}=UxNy$7IocLUz*baByVI!vd!q*I zhgwh=wUAM+zXFF+uXg?Kqqg|tWb&_5e2NC0)@!Ih0?ejV^+8RPj@sjF)Siz-Wn=>C zy1A%Kl)3ssR3@HBr8?sJ*P$}E5fx~o=L$PffqaS@_%v!|mynq3Dk`x1s8q-GH0y{V z)E=j!?#snTun;3ygu4DD>bi@r-iBI`_kco(LKLHO-fRG>qrlZCqEa;1EXQ5hP|?gT!DTVJd!&IvcIdtEfYF!`1I#5%qTT6j1?pEnp?nP><1`ke{j+ zRR0;&p}OL{hMMpuDkBfEf%}8~%r;Qpk>_7JsgR9PyyWRPyUsWlx!iD%NYWyjg zerCNn6f|I!^EFiJ>o5klp#s>2ihLi=#}*`Jiy!Q_=VNE;g{Z?i4WqFf6<`HM;Y!q& zuJ+qKYoMS*@d+}fwW0PZKFbfRuQQCit86?fW2LUX1ob?>jl7HOu z4^iD=kLdKMR%7xv2Xq zQ7c}Cx~>+Lsdq6(hwMEHTG38aiViuCV37KE*agpH99~AH^cHHOhp34Yhx&Wp1C^-( zsQbpC#w~XJGf`Vqj$TI!ODU|!S5S|~Kd6aPINFMQ5Gs&SNS94Uz2ld=_F7l3#}MsJ zsFk0>7`%XQ;T2Rsi^6ONz7!_^I>n7N#N&QcCcZ^Qd;xXpFQEeX8#O>wjz4e$s-A*c zNCxV#W}-5ZgIYiyYWyjv%$2(SnjG@43765Z3OAwxip}*eOv8!P^HBpva0b4KbMZWC zPX`Y3|Kk~oG+8++16xoF*^W)vgc^S;C$eUCHo57Y&PBmBL68hK4yH74N( z)C#vdcVjo|pJ7)#j#}w?)FHc$3h)*xlkKRpk;nr`E2rq YV5j_0PTt7j;pW}_3WLpY*_~tm1;98pv;Y7A diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.po b/amt/locale/nl_NL/LC_MESSAGES/messages.po index 61462cac..0d1a43f6 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" @@ -236,7 +236,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" @@ -252,7 +252,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" @@ -286,6 +286,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 " @@ -643,6 +647,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" @@ -659,7 +664,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" @@ -707,15 +712,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" @@ -734,7 +739,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" @@ -885,31 +890,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" diff --git a/amt/repositories/algorithms.py b/amt/repositories/algorithms.py index c7e3a48c..fe629c70 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 == 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 %} - -{% endfor %} +{% if request.state.htmx is defined and request.state.htmx %} + {% for msg in message %} + + {% 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..458b55fc 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 %} + Does not exist + {% 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) }}