From 0f5016fb73eb17ca56a4a3f3d1a3cab6462f0d6b Mon Sep 17 00:00:00 2001 From: robbertuittenbroek Date: Fri, 29 Nov 2024 13:28:14 +0100 Subject: [PATCH] Add member management for organization --- amt/api/forms/organization.py | 13 +- amt/api/navigation.py | 6 +- amt/api/routes/organizations.py | 112 +++++++++++++-- amt/core/log.py | 1 + amt/locale/base.pot | 123 ++++++++++++----- amt/locale/en_US/LC_MESSAGES/messages.po | 123 ++++++++++++----- amt/locale/nl_NL/LC_MESSAGES/messages.mo | Bin 13264 -> 13858 bytes amt/locale/nl_NL/LC_MESSAGES/messages.po | 125 ++++++++++++----- amt/repositories/organizations.py | 6 + amt/repositories/users.py | 25 +++- amt/schema/organization.py | 6 +- amt/schema/webform.py | 6 +- amt/services/organizations.py | 10 ++ amt/services/users.py | 3 + amt/site/static/scss/layout.scss | 7 +- amt/site/static/ts/amt.ts | 43 ++++++ amt/site/templates/macros/form_macros.html.j2 | 19 ++- amt/site/templates/macros/tabs.html.j2 | 17 +++ amt/site/templates/organizations/home.html.j2 | 17 +-- .../templates/organizations/members.html.j2 | 50 +++++++ .../parts/add_members_modal.html.j2 | 29 ++++ .../parts/members_results.html.j2 | 129 ++++++++++++++++++ .../parts/overview_results.html.j2 | 8 +- .../templates/parts/algorithm_search.html.j2 | 1 - tests/api/routes/test_organizations.py | 77 ++++++++++- tests/constants.py | 23 +++- tests/database_test_utils.py | 5 +- tests/repositories/test_organizations.py | 91 ++++++++++++ 28 files changed, 917 insertions(+), 158 deletions(-) create mode 100644 amt/site/templates/macros/tabs.html.j2 create mode 100644 amt/site/templates/organizations/members.html.j2 create mode 100644 amt/site/templates/organizations/parts/add_members_modal.html.j2 create mode 100644 amt/site/templates/organizations/parts/members_results.html.j2 create mode 100644 tests/repositories/test_organizations.py diff --git a/amt/api/forms/organization.py b/amt/api/forms/organization.py index 67785aad..115c9679 100644 --- a/amt/api/forms/organization.py +++ b/amt/api/forms/organization.py @@ -1,9 +1,17 @@ from gettext import NullTranslations -from amt.schema.webform import WebForm, WebFormField, WebFormFieldType, WebFormSearchField, WebFormSubmitButton +from amt.models import User +from amt.schema.webform import ( + WebForm, + WebFormField, + WebFormFieldType, + WebFormOption, + WebFormSearchField, + WebFormSubmitButton, +) -def get_organization_form(id: str, translations: NullTranslations) -> WebForm: +def get_organization_form(id: str, translations: NullTranslations, user: User | None) -> WebForm: _ = translations.gettext organization_form: WebForm = WebForm(id=id, post_url="/organizations/new") @@ -31,6 +39,7 @@ def get_organization_form(id: str, translations: NullTranslations) -> WebForm: placeholder=_("Search for a person..."), search_url="/organizations/users?returnType=search_select_field", query_var_name="query", + default_value=WebFormOption(str(user.id), user.name) if user else None, group="1", ), WebFormSubmitButton(label=_("Add organization"), group="1", name="submit"), diff --git a/amt/api/navigation.py b/amt/api/navigation.py index 9529a6b6..550ee208 100644 --- a/amt/api/navigation.py +++ b/amt/api/navigation.py @@ -31,7 +31,7 @@ class DisplayText(Enum): MODELCARD = "modelcard" DETAILS = "details" ORGANIZATIONS = "organizations" - MEMBERS = "people" + MEMBERS = "members" def get_translation(key: DisplayText, translations: NullTranslations) -> str: @@ -142,8 +142,8 @@ class Navigation: ORGANIZATIONS_ALGORITHMS = BaseNavigationItem( display_text=DisplayText.ALGORITHMS, url="/organizations/{organization_slug}/algorithms" ) - ORGANIZATIONS_PEOPLE = BaseNavigationItem( - display_text=DisplayText.MEMBERS, url="/organizations/{organization_slug}/people" + ORGANIZATIONS_MEMBERS = BaseNavigationItem( + display_text=DisplayText.MEMBERS, url="/organizations/{organization_slug}/members" ) diff --git a/amt/api/routes/organizations.py b/amt/api/routes/organizations.py index a95ff4de..a474d59b 100644 --- a/amt/api/routes/organizations.py +++ b/amt/api/routes/organizations.py @@ -20,12 +20,12 @@ from amt.api.routes.algorithm import UpdateFieldModel, set_path from amt.api.routes.shared import get_filters_and_sort_by from amt.core.authorization import get_user -from amt.core.exceptions import AMTNotFound, AMTRepositoryError +from amt.core.exceptions import AMTAuthorizationError, AMTNotFound, AMTRepositoryError from amt.core.internationalization import get_current_translation -from amt.models import Organization +from amt.models import Organization, User from amt.repositories.organizations import OrganizationsRepository from amt.repositories.users import UsersRepository -from amt.schema.organization import OrganizationBase, OrganizationNew, OrganizationSlug +from amt.schema.organization import OrganizationBase, OrganizationNew, OrganizationSlug, OrganizationUsers from amt.services.organizations import OrganizationsService router = APIRouter() @@ -35,7 +35,7 @@ def get_organization_tabs(request: Request, organization_slug: str) -> list[Navi request.state.path_variables = {"organization_slug": organization_slug} return resolve_navigation_items( - [Navigation.ORGANIZATIONS_INFO, Navigation.ORGANIZATIONS_ALGORITHMS, Navigation.ORGANIZATIONS_PEOPLE], + [Navigation.ORGANIZATIONS_INFO, Navigation.ORGANIZATIONS_ALGORITHMS, Navigation.ORGANIZATIONS_MEMBERS], request, ) @@ -65,14 +65,17 @@ async def get_users( @router.get("/new") async def get_new( request: Request, + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], ) -> HTMLResponse: breadcrumbs = resolve_base_navigation_items([Navigation.ORGANIZATIONS_ROOT, Navigation.ORGANIZATIONS_NEW], request) - - form = get_organization_form(id="organization", translations=get_current_translation(request)) - - context: dict[str, Any] = {"form": form, "breadcrumbs": breadcrumbs} - - return templates.TemplateResponse(request, "organizations/new.html.j2", context) + # todo (Robbert): make object SessionUser so it can be used as alternative for Database User + session_user = get_user(request) + if session_user: + user = await users_repository.find_by_id(session_user["sub"]) + form = get_organization_form(id="organization", translations=get_current_translation(request), user=user) + context: dict[str, Any] = {"form": form, "breadcrumbs": breadcrumbs} + return templates.TemplateResponse(request, "organizations/new.html.j2", context) + raise AMTAuthorizationError() @router.get("/") @@ -247,8 +250,91 @@ async def get_algorithms( return templates.TemplateResponse(request, "pages/under_construction.html.j2", {}) -@router.get("/{slug}/people") -async def get_people( +@router.delete("/{slug}/members/{user_id}") +async def remove_member( request: Request, + slug: str, + user_id: UUID, + organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], ) -> HTMLResponse: - return templates.TemplateResponse(request, "pages/under_construction.html.j2", {}) + # TODO (Robbert): add authorization and check if user and organization exist? + organization = await organizations_repository.find_by_slug(slug) + user: User | None = await users_repository.find_by_id(user_id) + if user: + await organizations_repository.remove_user(organization, user) + return templates.Redirect(request, f"/organizations/{slug}/members") + raise AMTAuthorizationError + + +@router.get("/{slug}/members/form") +async def get_members_form( + request: Request, + slug: str, +) -> HTMLResponse: + form = get_organization_form(id="organization", translations=get_current_translation(request), user=None) + context: dict[str, Any] = {"form": form, "slug": slug} + return templates.TemplateResponse(request, "organizations/parts/add_members_modal.html.j2", context) + + +@router.put("/{slug}/members", response_class=HTMLResponse) +async def add_new_members( + request: Request, + slug: str, + organizations_service: Annotated[OrganizationsService, Depends(OrganizationsService)], + organization_users: OrganizationUsers, +) -> HTMLResponse: + organization = await organizations_service.find_by_slug(slug) + await organizations_service.add_users(organization, organization_users.user_ids) + return templates.Redirect(request, f"/organizations/{slug}/members") + + +@router.get("/{slug}/members") +async def get_members( + request: Request, + slug: str, + organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], + 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) + 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, + BaseNavigationItem(custom_display_text=organization.name, url="/organizations/{organization_slug}"), + Navigation.ORGANIZATIONS_MEMBERS, + ], + request, + ) + + filters["organization-id"] = str(organization.id) + members = await users_repository.find_all(search=search, sort=sort_by, filters=filters) + + context: dict[str, Any] = { + "slug": organization.slug, + "breadcrumbs": breadcrumbs, + "tab_items": tab_items, + "members": members, + "next": next, + "limit": limit, + "start": skip, + "search": search, + "sort_by": sort_by, + "members_length": len(members), + "filters": localized_filters, + "include_filters": False, + "organization_filters": get_localized_organization_filters(request), + } + + if request.state.htmx: + if drop_filters: + context.update({"include_filters": True}) + return templates.TemplateResponse(request, "organizations/parts/members_results.html.j2", context) + else: + context.update({"include_filters": True}) + return templates.TemplateResponse(request, "organizations/members.html.j2", context) diff --git a/amt/core/log.py b/amt/core/log.py index 994cd0e1..c68e69f5 100644 --- a/amt/core/log.py +++ b/amt/core/log.py @@ -32,6 +32,7 @@ "loggers": { "": {"handlers": ["console", "file"], "level": "DEBUG", "propagate": False}, "httpcore": {"handlers": ["console", "file"], "level": "ERROR", "propagate": False}, + "aiosqlite": {"handlers": ["console", "file"], "level": "INFO", "propagate": False}, }, } diff --git a/amt/locale/base.pot b/amt/locale/base.pot index bf83cd3f..0c0fa0e6 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -44,7 +44,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:48 +#: amt/site/templates/parts/algorithm_search.html.j2:47 #: amt/site/templates/parts/filter_list.html.j2:71 msgid "Lifecycle" msgstr "" @@ -151,8 +151,9 @@ msgstr "" msgid "Organizations" msgstr "" -#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:45 -#: amt/site/templates/organizations/parts/overview_results.html.j2:130 +#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:32 +#: amt/site/templates/organizations/parts/members_results.html.j2:6 +#: amt/site/templates/organizations/parts/overview_results.html.j2:132 msgid "Members" msgstr "" @@ -197,39 +198,42 @@ msgstr "" msgid "Organization" msgstr "" -#: amt/api/forms/organization.py:15 +#: 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:25 +#: amt/site/templates/organizations/home.html.j2:12 +#: amt/site/templates/organizations/parts/members_results.html.j2:118 msgid "Name" msgstr "" -#: amt/api/forms/organization.py:16 +#: amt/api/forms/organization.py:24 msgid "Name of the organization" msgstr "" -#: amt/api/forms/organization.py:23 +#: amt/api/forms/organization.py:31 msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" -#: amt/api/forms/organization.py:24 -#: amt/site/templates/organizations/home.html.j2:29 +#: amt/api/forms/organization.py:32 +#: amt/site/templates/organizations/home.html.j2:16 msgid "Slug" msgstr "" -#: amt/api/forms/organization.py:25 +#: amt/api/forms/organization.py:33 msgid "The slug for this organization" msgstr "" -#: amt/api/forms/organization.py:30 +#: amt/api/forms/organization.py:38 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:4 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:23 msgid "Add members" msgstr "" -#: amt/api/forms/organization.py:31 +#: amt/api/forms/organization.py:39 msgid "Search for a person..." msgstr "" -#: amt/api/forms/organization.py:36 +#: amt/api/forms/organization.py:45 msgid "Add organization" msgstr "" @@ -309,11 +313,13 @@ msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:39 #: amt/site/templates/algorithms/new.html.j2:134 +#: amt/site/templates/organizations/members.html.j2:33 msgid "Yes" msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:44 #: amt/site/templates/algorithms/new.html.j2:144 +#: amt/site/templates/organizations/members.html.j2:36 msgid "No" msgstr "" @@ -429,7 +435,7 @@ msgid "Algorithm code" msgstr "" #: amt/site/templates/algorithms/details_info.html.j2:32 -#: amt/site/templates/organizations/parts/overview_results.html.j2:132 +#: amt/site/templates/organizations/parts/overview_results.html.j2:134 #: amt/site/templates/pages/assessment_card.html.j2:7 #: amt/site/templates/pages/model_card.html.j2:6 #: amt/site/templates/pages/system_card.html.j2:4 @@ -486,6 +492,7 @@ msgstr "" #: amt/site/templates/algorithms/details_measure_modal.html.j2:88 #: amt/site/templates/macros/editable.html.j2:87 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" msgstr "" @@ -617,10 +624,26 @@ msgstr "" msgid "There is one error:" msgstr "" -#: amt/site/templates/layouts/base.html.j2:1 +#: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" +#: amt/site/templates/macros/form_macros.html.j2:52 +msgid "Are you sure you want to remove " +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:52 +msgid " from this organization? " +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:55 +msgid "Delete" +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:56 +msgid "Delete member" +msgstr "" + #: amt/site/templates/macros/table_row.html.j2:19 msgid " ago" msgstr "" @@ -641,40 +664,75 @@ msgstr "" msgid "Unknown" msgstr "" -#: amt/site/templates/organizations/home.html.j2:33 +#: amt/site/templates/organizations/home.html.j2:20 msgid "Created at" msgstr "" -#: amt/site/templates/organizations/home.html.j2:37 +#: amt/site/templates/organizations/home.html.j2:24 msgid "Created by" msgstr "" -#: amt/site/templates/organizations/home.html.j2:41 +#: amt/site/templates/organizations/home.html.j2:28 msgid "Modified at" msgstr "" +#: amt/site/templates/organizations/members.html.j2:24 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:8 +msgid "Close" +msgstr "" + #: amt/site/templates/organizations/new.html.j2:8 msgid "Create an organization" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:11 -msgid "New organization" +#: amt/site/templates/organizations/parts/members_results.html.j2:15 +msgid "Add member" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:27 +#: 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 msgid "Search" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:34 +#: amt/site/templates/organizations/parts/members_results.html.j2:36 +msgid "Find member..." +msgstr "" + +#: amt/site/templates/organizations/parts/members_results.html.j2:65 +#, python-format +msgid "" +"%(members_length)s result\n" +" " +msgid_plural "" +"\n" +" %(members_length)s results" +msgstr[0] "" +msgstr[1] "" + +#: amt/site/templates/organizations/parts/members_results.html.j2:70 +#: amt/site/templates/organizations/parts/overview_results.html.j2:86 +#: amt/site/templates/parts/filter_list.html.j2:16 +msgid "for" +msgstr "" + +#: amt/site/templates/organizations/parts/members_results.html.j2:121 +msgid "Action" +msgstr "" + +#: amt/site/templates/organizations/parts/overview_results.html.j2:11 +msgid "New organization" +msgstr "" + +#: amt/site/templates/organizations/parts/overview_results.html.j2:33 msgid "Find organizations..." msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:51 +#: amt/site/templates/organizations/parts/overview_results.html.j2:50 msgid "Organisation Type" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:79 +#: amt/site/templates/organizations/parts/overview_results.html.j2:81 #, python-format msgid "" "%(organizations_length)s result\n" @@ -685,12 +743,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:84 -#: amt/site/templates/parts/filter_list.html.j2:16 -msgid "for" -msgstr "" - -#: amt/site/templates/organizations/parts/overview_results.html.j2:127 +#: amt/site/templates/organizations/parts/overview_results.html.j2:129 msgid "Organization name" msgstr "" @@ -790,23 +843,23 @@ msgstr "" msgid "Find algorithm..." msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:53 +#: amt/site/templates/parts/algorithm_search.html.j2:52 msgid "Select lifecycle" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:62 +#: amt/site/templates/parts/algorithm_search.html.j2:61 msgid "Category" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:67 +#: amt/site/templates/parts/algorithm_search.html.j2:66 msgid "Select publication category" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:85 +#: amt/site/templates/parts/algorithm_search.html.j2:84 msgid "Group by" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:95 +#: amt/site/templates/parts/algorithm_search.html.j2:94 msgid "Select group by" msgstr "" diff --git a/amt/locale/en_US/LC_MESSAGES/messages.po b/amt/locale/en_US/LC_MESSAGES/messages.po index 509e10d3..38587da6 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -45,7 +45,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:48 +#: amt/site/templates/parts/algorithm_search.html.j2:47 #: amt/site/templates/parts/filter_list.html.j2:71 msgid "Lifecycle" msgstr "" @@ -152,8 +152,9 @@ msgstr "" msgid "Organizations" msgstr "" -#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:45 -#: amt/site/templates/organizations/parts/overview_results.html.j2:130 +#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:32 +#: amt/site/templates/organizations/parts/members_results.html.j2:6 +#: amt/site/templates/organizations/parts/overview_results.html.j2:132 msgid "Members" msgstr "" @@ -198,39 +199,42 @@ msgstr "" msgid "Organization" msgstr "" -#: amt/api/forms/organization.py:15 +#: 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:25 +#: amt/site/templates/organizations/home.html.j2:12 +#: amt/site/templates/organizations/parts/members_results.html.j2:118 msgid "Name" msgstr "" -#: amt/api/forms/organization.py:16 +#: amt/api/forms/organization.py:24 msgid "Name of the organization" msgstr "" -#: amt/api/forms/organization.py:23 +#: amt/api/forms/organization.py:31 msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" -#: amt/api/forms/organization.py:24 -#: amt/site/templates/organizations/home.html.j2:29 +#: amt/api/forms/organization.py:32 +#: amt/site/templates/organizations/home.html.j2:16 msgid "Slug" msgstr "" -#: amt/api/forms/organization.py:25 +#: amt/api/forms/organization.py:33 msgid "The slug for this organization" msgstr "" -#: amt/api/forms/organization.py:30 +#: amt/api/forms/organization.py:38 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:4 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:23 msgid "Add members" msgstr "" -#: amt/api/forms/organization.py:31 +#: amt/api/forms/organization.py:39 msgid "Search for a person..." msgstr "" -#: amt/api/forms/organization.py:36 +#: amt/api/forms/organization.py:45 msgid "Add organization" msgstr "" @@ -310,11 +314,13 @@ msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:39 #: amt/site/templates/algorithms/new.html.j2:134 +#: amt/site/templates/organizations/members.html.j2:33 msgid "Yes" msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:44 #: amt/site/templates/algorithms/new.html.j2:144 +#: amt/site/templates/organizations/members.html.j2:36 msgid "No" msgstr "" @@ -430,7 +436,7 @@ msgid "Algorithm code" msgstr "" #: amt/site/templates/algorithms/details_info.html.j2:32 -#: amt/site/templates/organizations/parts/overview_results.html.j2:132 +#: amt/site/templates/organizations/parts/overview_results.html.j2:134 #: amt/site/templates/pages/assessment_card.html.j2:7 #: amt/site/templates/pages/model_card.html.j2:6 #: amt/site/templates/pages/system_card.html.j2:4 @@ -487,6 +493,7 @@ msgstr "" #: amt/site/templates/algorithms/details_measure_modal.html.j2:88 #: amt/site/templates/macros/editable.html.j2:87 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" msgstr "" @@ -618,10 +625,26 @@ msgstr "" msgid "There is one error:" msgstr "" -#: amt/site/templates/layouts/base.html.j2:1 +#: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" +#: amt/site/templates/macros/form_macros.html.j2:52 +msgid "Are you sure you want to remove " +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:52 +msgid " from this organization? " +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:55 +msgid "Delete" +msgstr "" + +#: amt/site/templates/macros/form_macros.html.j2:56 +msgid "Delete member" +msgstr "" + #: amt/site/templates/macros/table_row.html.j2:19 msgid " ago" msgstr "" @@ -642,40 +665,75 @@ msgstr "" msgid "Unknown" msgstr "" -#: amt/site/templates/organizations/home.html.j2:33 +#: amt/site/templates/organizations/home.html.j2:20 msgid "Created at" msgstr "" -#: amt/site/templates/organizations/home.html.j2:37 +#: amt/site/templates/organizations/home.html.j2:24 msgid "Created by" msgstr "" -#: amt/site/templates/organizations/home.html.j2:41 +#: amt/site/templates/organizations/home.html.j2:28 msgid "Modified at" msgstr "" +#: amt/site/templates/organizations/members.html.j2:24 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:8 +msgid "Close" +msgstr "" + #: amt/site/templates/organizations/new.html.j2:8 msgid "Create an organization" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:11 -msgid "New organization" +#: amt/site/templates/organizations/parts/members_results.html.j2:15 +msgid "Add member" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:27 +#: 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 msgid "Search" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:34 +#: amt/site/templates/organizations/parts/members_results.html.j2:36 +msgid "Find member..." +msgstr "" + +#: amt/site/templates/organizations/parts/members_results.html.j2:65 +#, fuzzy, python-format +msgid "" +"%(members_length)s result\n" +" " +msgid_plural "" +"\n" +" %(members_length)s results" +msgstr[0] "" +msgstr[1] "" + +#: amt/site/templates/organizations/parts/members_results.html.j2:70 +#: amt/site/templates/organizations/parts/overview_results.html.j2:86 +#: amt/site/templates/parts/filter_list.html.j2:16 +msgid "for" +msgstr "" + +#: amt/site/templates/organizations/parts/members_results.html.j2:121 +msgid "Action" +msgstr "" + +#: amt/site/templates/organizations/parts/overview_results.html.j2:11 +msgid "New organization" +msgstr "" + +#: amt/site/templates/organizations/parts/overview_results.html.j2:33 msgid "Find organizations..." msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:51 +#: amt/site/templates/organizations/parts/overview_results.html.j2:50 msgid "Organisation Type" msgstr "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:79 +#: amt/site/templates/organizations/parts/overview_results.html.j2:81 #, fuzzy, python-format msgid "" "%(organizations_length)s result\n" @@ -686,12 +744,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: amt/site/templates/organizations/parts/overview_results.html.j2:84 -#: amt/site/templates/parts/filter_list.html.j2:16 -msgid "for" -msgstr "" - -#: amt/site/templates/organizations/parts/overview_results.html.j2:127 +#: amt/site/templates/organizations/parts/overview_results.html.j2:129 msgid "Organization name" msgstr "" @@ -791,23 +844,23 @@ msgstr "" msgid "Find algorithm..." msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:53 +#: amt/site/templates/parts/algorithm_search.html.j2:52 msgid "Select lifecycle" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:62 +#: amt/site/templates/parts/algorithm_search.html.j2:61 msgid "Category" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:67 +#: amt/site/templates/parts/algorithm_search.html.j2:66 msgid "Select publication category" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:85 +#: amt/site/templates/parts/algorithm_search.html.j2:84 msgid "Group by" msgstr "" -#: amt/site/templates/parts/algorithm_search.html.j2:95 +#: amt/site/templates/parts/algorithm_search.html.j2:94 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 f6be25089e6678866fe827d27784da827a110288..f86357c1654571b4b8d2f8ee5698db61495c45f5 100644 GIT binary patch delta 3439 zcmZ|Q32YTb9LMp04@>ERJ%H9Ku$0!e2y#>oL4{Tni->_HQ~^O~ms07W?-k+!JS3oC zsuiB7Q55kYhy)WJ5d(5au*MpwibgRY5>2p5NReow;PL&rV@ynFmiL)scjiC;nO#1q zeYHOFO;+N1<8MFzuHs+MB-Q@=OG`6bNi`j(<2p>lPP_uo;=DMsUhQ1e5uohFzA{L^MLBlf4##U4yub~DwfSK6g>Zh=PdKYS-UOcRR&pF&V z0c&ZWj+1a7mSA^oz6(n+iTSOC!YCS|=;6o6*mefF>>@wiu}78}i?v)-V1>wK*YTs3 zO~hoJf_ z_N^7C;Y&zrtqXPkMeK$NJ%jdCR3>sTq6o_N*xzVWpslCYmH5968P-2)wM6je$-n~hj*LuuhZE^gChG8m9m7KV8RsCgxRPw z;Gt4K5Ov>JREEl3{WerUccM~W?fMs@GWq~2z$MPr5ekar8PverQ7hYz%ETd5U|*xQ z;5;T^5{=sP6x4IM7>^~m6bGRW@mAy!+l{*aGt@ZWq81i8Pr;+0D6}X2Q5~hOJ|2~_ zX{Z50sI6FzOK=TpMM-(V8OTBfG|X9!90#jJ1+)P5{Da825qp$^QnSgq4HdvHRLT#b zGIGRu6qWj~klnJAsDXcVCS4f>&>OWCMaX4i`MD0OP~)}W?Rx*$QqX`WuoSIi^z4)=fbLP=j>YgQyHYiyChWCNjTmR{>+F z6~Bwxql2hJ^abjH6UZX%J0wQ?6G@@v@uM>_#(5Jeqvfb^?{MwY@K);6Q44zsBZ}xv z3OYP{ocmA{zK`o!@rU>X^?UjUpVA*tftBW)72{ZJ;l3IiL;W1DlqMQf5M(Nhddr?b zjk_H+@6H19uRKe{QT>xpE3I|?A>=&TO6SWMq5dw; z#?e=sjlk!ehp(nrr!j#IzY)`%lTd-JMjfsfP+PMVmAaj-egJhA+FkvW^C#4bf5C2e z1!*FQ)*m%)8EV0m5ej-e>QJxOGSq}En1~zD!%a93526AoEC~+R5a-RPfvd0*>rije zUhIzTs6akPE#Np3n?-(b4H*N2Ok9OYbQGcj8-$l(8R|4obnP=yhwy&Xp0^-F*@vht zI)@5K-*=gX`VbYMGIm2yk60xIy_X9y6<73gVHMlWDG_8bzl=VL?>jif-Im7~7h51`stxcXD*QQwHlzyZ`s z598zbB`UBvLzPmq`%tI*X}laaqXKycm6>+b0!|Gj|9UM>)1V2v4GRX!MorMy)r(Op zD@C2+QK$@!Lk&CumBG7Qdjo2s5Gs&0s6F3^x8PpX^9jSr{}c+z!-Ku7LSnJm$Yrhk zXvGIn5qIDl_yuaYQ48saxWbp1MZ-z# zfj^^C7dIj}T$!lDmWRq%f7F>MLw#uO#&}$c8hAPC`PHby`8;aHo3H{mqcUF5MM0^$ zh)Qwx$Y7ux)IfO{9p{aR4)@BV=Tqx*=ZBgayygXUVXrAvJGZg!;knIqO^xHc*ga|U zC~k znvt6n@*A2K`_Ua4PbStkh5hJ_nVr#oS%XvS{bv8)n=7)G{5w_bz3e{e(JjMDV#mDJ zxY3uGbne_{D=fZLXS3f(P+oLZesxY6k@+^g$*=Y9_e0^PM!(T(Zt`QN^H;@3uPy9I ktgK&D7yGR+Gj3f`Nyh*0ixm~U6_@${RzLP>@n0!_1DmCj3jhEB delta 3061 zcmYM#eN5F=9LMqRMFB7JjF5`L6%hmy@eyY}%r-3%HS-~*)W%43vSwKgC)Zn+Y0NTr zWwm9fjf`81bk~@zO{H`>=R*6#Ni&Bl)0qT0o9oGXfB2m(cD=82&hPg-=llJh^Sgdf zccC_PHGN={@z>735&SzJtKR>A4TH^IqZ`B(7?Wr=5LaQf&uk5@qu-HamW*Th8ja=1 zBlaYwVl8H1J!+lZ$R}hGzS9^u?hV*E%%|Ul33%UK_osOGWjn`VE!Ssb1%8A@cn4SF z2v&{57qJ{0kV|$9wccs;v%X!TK@!%DtYtlzj(?*jNa5~5n1wl*i&;1exy6>F?qBD| zx1a)l+xZ@=juLV09P{vo?4Y#nAeyO zOhipwiOOs>GKE#60;@-relHHd7UV^-R@8ItAsR6>zQG`#L!Ht`A28!%EBVp`n@|&N zLuIxX1K5n(s*`T~qU&EpRjd#7d@9kXGCA0Qg{Xu=Ei|l0QX9X&-<*1TZqbgG8d=6FVmyjGSh|C+ZeO|*_Pyrl6ZN(YP#x9(UzoRBB z9&WY($D=0Pij#0V7GgX0pC!~5-oiY7sQOR~XYlK@7K<@Q?|&nWhZ%^V_U<++fS8eH zd@PGEm3T60!g;7bDxFJE8LvQX(bK3iv>A2(R%C5^1(nc#R0WUY0M@t5YTy-ANxM-C z{Nl#@a31};sLUQ>zZB3TsI#-m`4nowwb(?)&tN0{zgVpTUk`YJUBum7@4}EKUYF;U z=2O&bbsDw6O;knxM7^e|T%Fv(8rIOL z$~T*WUpRYFhi%Llvzb_mTBr_n7`LHH{uZht@49{qYLDAo|BSN}S<|{urT+&t-^c>O zP=@0Rywg4x^}1A{7Fv(mih2xS5Ua5nbw+%gIGKq{u?Y1R1W^GtVm$6c1-c*kSgY$_ z2+>fcS1=B*qh7n)7>mBK-U9Kc@odx~D#PhmiAA^*wIxSUnVmvjefthokz20s<7T~0 z!%zW*7SZ4pw<^?)n{g^OqB7|~P5dn?pdU~f|A?y8J=8?8#a^Y-P|s(fo?nQZTdP8< zZuQ9STC+DEvae|<^J}P5-$Z55gPP!9)Pjj6-hBZaM86RIScVFGIx4^=&L?m%{neO& z8&CnifU4wnOw%!ZhlUo8p!U2CRk{_Mh>*K;$pmr zx;~0Y(y$!)@w54;K-Z%Z2|9ORI{h~%vj5364l%dNmPVqQ6=j{orzx54@vwa z@29r}HE}8G`KhSq=b7#e3#6P?BIp;1%9a}wL4{-hdT zcuD@q@b%<|=;oAFG2x!H#&9tGMtEaJLG(bz7GJb5b4XIOCU1}K%L!NJSBKL}(xaF2 epN|ReD%=|#H8#l?o>r6_-B1+qMe|GUC;SI4KRb>9 diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.po b/amt/locale/nl_NL/LC_MESSAGES/messages.po index c327fabc..35fe9b80 100644 --- a/amt/locale/nl_NL/LC_MESSAGES/messages.po +++ b/amt/locale/nl_NL/LC_MESSAGES/messages.po @@ -45,7 +45,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:48 +#: amt/site/templates/parts/algorithm_search.html.j2:47 #: amt/site/templates/parts/filter_list.html.j2:71 msgid "Lifecycle" msgstr "Levenscyclus" @@ -152,8 +152,9 @@ msgstr "Instrumenten" msgid "Organizations" msgstr "Organisaties" -#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:45 -#: amt/site/templates/organizations/parts/overview_results.html.j2:130 +#: amt/api/navigation.py:64 amt/site/templates/organizations/home.html.j2:32 +#: amt/site/templates/organizations/parts/members_results.html.j2:6 +#: amt/site/templates/organizations/parts/overview_results.html.j2:132 msgid "Members" msgstr "Leden" @@ -198,41 +199,44 @@ msgstr "Selecteer organisatie" msgid "Organization" msgstr "Organisatie" -#: amt/api/forms/organization.py:15 +#: 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:25 +#: amt/site/templates/organizations/home.html.j2:12 +#: amt/site/templates/organizations/parts/members_results.html.j2:118 msgid "Name" msgstr "Naam" -#: amt/api/forms/organization.py:16 +#: amt/api/forms/organization.py:24 msgid "Name of the organization" msgstr "Naam van de organisatie" -#: amt/api/forms/organization.py:23 +#: amt/api/forms/organization.py:31 msgid "The slug is the web path, like /organizations/my-organization-name" msgstr "" "Een slug is het pad in het webadres, zoals /organizations/mijn-" "organisatie-naam" -#: amt/api/forms/organization.py:24 -#: amt/site/templates/organizations/home.html.j2:29 +#: amt/api/forms/organization.py:32 +#: amt/site/templates/organizations/home.html.j2:16 msgid "Slug" msgstr "Slug" -#: amt/api/forms/organization.py:25 +#: amt/api/forms/organization.py:33 msgid "The slug for this organization" msgstr "Het web-pad (slug) voor deze organisatie" -#: amt/api/forms/organization.py:30 +#: amt/api/forms/organization.py:38 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:4 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:23 msgid "Add members" msgstr "Voeg personen toe" -#: amt/api/forms/organization.py:31 +#: amt/api/forms/organization.py:39 msgid "Search for a person..." msgstr "Zoek een persoon..." -#: amt/api/forms/organization.py:36 +#: amt/api/forms/organization.py:45 msgid "Add organization" msgstr "Organisatie toevoegen" @@ -322,11 +326,13 @@ msgstr "" #: amt/site/templates/algorithms/details_base.html.j2:39 #: amt/site/templates/algorithms/new.html.j2:134 +#: amt/site/templates/organizations/members.html.j2:33 msgid "Yes" msgstr "Ja" #: amt/site/templates/algorithms/details_base.html.j2:44 #: amt/site/templates/algorithms/new.html.j2:144 +#: amt/site/templates/organizations/members.html.j2:36 msgid "No" msgstr "Nee" @@ -442,7 +448,7 @@ msgid "Algorithm code" msgstr "algoritme code" #: amt/site/templates/algorithms/details_info.html.j2:32 -#: amt/site/templates/organizations/parts/overview_results.html.j2:132 +#: amt/site/templates/organizations/parts/overview_results.html.j2:134 #: amt/site/templates/pages/assessment_card.html.j2:7 #: amt/site/templates/pages/model_card.html.j2:6 #: amt/site/templates/pages/system_card.html.j2:4 @@ -501,6 +507,7 @@ msgstr "Opslaan" #: amt/site/templates/algorithms/details_measure_modal.html.j2:88 #: amt/site/templates/macros/editable.html.j2:87 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" msgstr "Annuleren" @@ -637,10 +644,26 @@ msgstr "Er zijn enkele fouten" msgid "There is one error:" msgstr "Er is één fout:" -#: amt/site/templates/layouts/base.html.j2:1 +#: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "Algoritme Management Toolkit" +#: amt/site/templates/macros/form_macros.html.j2:52 +msgid "Are you sure you want to remove " +msgstr "Weet u zeker dat u uw algoritmische systeem wilt verwijderen " + +#: amt/site/templates/macros/form_macros.html.j2:52 +msgid " from this organization? " +msgstr "Het web-pad (slug) voor deze organisatie" + +#: amt/site/templates/macros/form_macros.html.j2:55 +msgid "Delete" +msgstr "Verwijder" + +#: amt/site/templates/macros/form_macros.html.j2:56 +msgid "Delete member" +msgstr "Voeg personen toe" + #: amt/site/templates/macros/table_row.html.j2:19 msgid " ago" msgstr "geleden" @@ -661,40 +684,75 @@ msgstr "Beoordelen" msgid "Unknown" msgstr "Onbekend" -#: amt/site/templates/organizations/home.html.j2:33 +#: amt/site/templates/organizations/home.html.j2:20 msgid "Created at" msgstr "Aangemaakt op" -#: amt/site/templates/organizations/home.html.j2:37 +#: amt/site/templates/organizations/home.html.j2:24 msgid "Created by" msgstr "Aangemaakt door" -#: amt/site/templates/organizations/home.html.j2:41 +#: amt/site/templates/organizations/home.html.j2:28 msgid "Modified at" msgstr "Bijgewerkt op" +#: amt/site/templates/organizations/members.html.j2:24 +#: amt/site/templates/organizations/parts/add_members_modal.html.j2:8 +msgid "Close" +msgstr "Sluiten" + #: amt/site/templates/organizations/new.html.j2:8 msgid "Create an organization" msgstr "Organisatie aanmaken" -#: amt/site/templates/organizations/parts/overview_results.html.j2:11 -msgid "New organization" -msgstr "Nieuwe organisatie" +#: amt/site/templates/organizations/parts/members_results.html.j2:15 +msgid "Add member" +msgstr "Voeg personen toe" -#: amt/site/templates/organizations/parts/overview_results.html.j2:27 +#: 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 msgid "Search" msgstr "Zoek" -#: amt/site/templates/organizations/parts/overview_results.html.j2:34 +#: amt/site/templates/organizations/parts/members_results.html.j2:36 +msgid "Find member..." +msgstr "Voeg personen toe" + +#: amt/site/templates/organizations/parts/members_results.html.j2:65 +#, python-format +msgid "" +"%(members_length)s result\n" +" " +msgid_plural "" +"\n" +" %(members_length)s results" +msgstr[0] "%(members_length)s resultaat" +msgstr[1] "%(members_length)s resultaten" + +#: amt/site/templates/organizations/parts/members_results.html.j2:70 +#: amt/site/templates/organizations/parts/overview_results.html.j2:86 +#: amt/site/templates/parts/filter_list.html.j2:16 +msgid "for" +msgstr "voor" + +#: amt/site/templates/organizations/parts/members_results.html.j2:121 +msgid "Action" +msgstr "Actie" + +#: amt/site/templates/organizations/parts/overview_results.html.j2:11 +msgid "New organization" +msgstr "Nieuwe organisatie" + +#: amt/site/templates/organizations/parts/overview_results.html.j2:33 msgid "Find organizations..." msgstr "Zoek een organisatie..." -#: amt/site/templates/organizations/parts/overview_results.html.j2:51 +#: amt/site/templates/organizations/parts/overview_results.html.j2:50 msgid "Organisation Type" msgstr "Organisatie Type" -#: amt/site/templates/organizations/parts/overview_results.html.j2:79 +#: amt/site/templates/organizations/parts/overview_results.html.j2:81 #, python-format msgid "" "%(organizations_length)s result\n" @@ -705,12 +763,7 @@ msgid_plural "" msgstr[0] "%(organizations_length)s resultaat" msgstr[1] "%(organizations_length)s resultaten" -#: amt/site/templates/organizations/parts/overview_results.html.j2:84 -#: amt/site/templates/parts/filter_list.html.j2:16 -msgid "for" -msgstr "voor" - -#: amt/site/templates/organizations/parts/overview_results.html.j2:127 +#: amt/site/templates/organizations/parts/overview_results.html.j2:129 msgid "Organization name" msgstr "Organisatie naam" @@ -817,23 +870,23 @@ msgstr "Nieuw algoritme" msgid "Find algorithm..." msgstr "Vind algoritme..." -#: amt/site/templates/parts/algorithm_search.html.j2:53 +#: amt/site/templates/parts/algorithm_search.html.j2:52 msgid "Select lifecycle" msgstr "Selecteer levenscyclus" -#: amt/site/templates/parts/algorithm_search.html.j2:62 +#: amt/site/templates/parts/algorithm_search.html.j2:61 msgid "Category" msgstr "Categorie" -#: amt/site/templates/parts/algorithm_search.html.j2:67 +#: amt/site/templates/parts/algorithm_search.html.j2:66 msgid "Select publication category" msgstr "Selecteer publicatiecategorie" -#: amt/site/templates/parts/algorithm_search.html.j2:85 +#: amt/site/templates/parts/algorithm_search.html.j2:84 msgid "Group by" msgstr "Groeperen op" -#: amt/site/templates/parts/algorithm_search.html.j2:95 +#: amt/site/templates/parts/algorithm_search.html.j2:94 msgid "Select group by" msgstr "Selecteer groepering" diff --git a/amt/repositories/organizations.py b/amt/repositories/organizations.py index ce3c1061..afe9b8c1 100644 --- a/amt/repositories/organizations.py +++ b/amt/repositories/organizations.py @@ -135,3 +135,9 @@ async def find_by_id_and_user_id(self, organization_id: int, user_id: str | UUID except NoResultFound as e: logger.exception("Organization not found") raise AMTRepositoryError from e + + async def remove_user(self, organization: Organization, user: User) -> Organization: + organization.users.remove(user) # pyright: ignore[reportUnknownMemberType] + await self.session.commit() + await self.session.refresh(organization) + return organization diff --git a/amt/repositories/users.py b/amt/repositories/users.py index f9a37eab..09c38a51 100644 --- a/amt/repositories/users.py +++ b/amt/repositories/users.py @@ -4,14 +4,14 @@ from uuid import UUID from fastapi import Depends -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.exc import NoResultFound, SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import lazyload from sqlalchemy_utils import escape_like # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType] from amt.core.exceptions import AMTRepositoryError -from amt.models import User +from amt.models import Organization, User from amt.repositories.deps import get_session logger = logging.getLogger(__name__) @@ -25,23 +25,40 @@ class UsersRepository: def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]) -> None: self.session = session - async def find_all(self, search: str | None = None, limit: int | None = None) -> Sequence[User]: + async def find_all( + self, + search: str | None = None, + sort: dict[str, str] | None = None, + filters: dict[str, str] | None = None, + skip: int | None = None, + limit: int | None = None, + ) -> Sequence[User]: statement = select(User) if search: statement = statement.filter(User.name.ilike(f"%{escape_like(search)}%")) + if filters and "organization-id" in filters: + statement = statement.where(User.organizations.any(Organization.id == int(filters["organization-id"]))) + if sort: + if "name" in sort and sort["name"] == "ascending": + statement = statement.order_by(func.lower(User.name).asc()) + elif "name" in sort and sort["name"] == "descending": + statement = statement.order_by(func.lower(User.name).desc()) # https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#lazy-loading statement = statement.options(lazyload(User.organizations)) + if skip: + statement = statement.offset(skip) if limit: statement = statement.limit(limit) return (await self.session.execute(statement)).scalars().all() - async def find_by_id(self, id: UUID) -> User | None: + async def find_by_id(self, id: UUID | str) -> User | None: """ Returns the user with the given id. :param id: the id of the user to find :return: the user with the given id or an exception if no user was found """ + id = UUID(id) if isinstance(id, str) else id statement = select(User).where(User.id == id) # https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#lazy-loading statement = statement.options(lazyload(User.organizations)) diff --git a/amt/schema/organization.py b/amt/schema/organization.py index 9f1e6a5e..32207943 100644 --- a/amt/schema/organization.py +++ b/amt/schema/organization.py @@ -10,9 +10,13 @@ class OrganizationSlug(BaseModel): slug: str = Field(min_length=3, max_length=64, pattern=r"^[a-z0-9-_]*{3,64}$") -class OrganizationNew(OrganizationBase, OrganizationSlug): +class OrganizationUsers(BaseModel): user_ids: list[str] | str @field_validator("user_ids") def ensure_list(cls, v: list[str] | str) -> list[str]: return v if isinstance(v, list) else [v] + + +class OrganizationNew(OrganizationBase, OrganizationSlug, OrganizationUsers): + pass diff --git a/amt/schema/webform.py b/amt/schema/webform.py index a0d8925c..9c58ee5b 100644 --- a/amt/schema/webform.py +++ b/amt/schema/webform.py @@ -38,7 +38,7 @@ def __init__(self, type: WebFormFieldType, name: str, label: str, group: str | N class WebFormField(WebFormBaseField): placeholder: str | None - default_value: str | None + default_value: str | WebFormOption | None options: list[WebFormOption] | None validators: list[Any] description: str | None @@ -50,7 +50,7 @@ def __init__( name: str, label: str, placeholder: str | None = None, - default_value: str | None = None, + default_value: str | WebFormOption | None = None, options: list[WebFormOption] | None = None, attributes: dict[str, str] | None = None, description: str | None = None, @@ -75,7 +75,7 @@ def __init__( name: str, label: str, placeholder: str | None = None, - default_value: str | None = None, + default_value: str | None | WebFormOption = None, options: list[WebFormOption] | None = None, attributes: dict[str, str] | None = None, group: str | None = None, diff --git a/amt/services/organizations.py b/amt/services/organizations.py index 59f21c96..1b0ee64d 100644 --- a/amt/services/organizations.py +++ b/amt/services/organizations.py @@ -47,3 +47,13 @@ async def get_organizations_for_user(self, user_id: str | UUID | None) -> Sequen filters={"organization-type": OrganizationFilterOptions.MY_ORGANIZATIONS.value}, user_id=user_id, ) + + async def add_users(self, organization: Organization, user_ids: list[str] | str) -> Organization: + new_users: list[User | None] = [await self.users_service.find_by_id(user_id) for user_id in user_ids] + for user in new_users: + if user not in organization.users: # pyright: ignore[reportUnknownMemberType] + organization.users.append(user) # pyright: ignore[reportUnknownMemberType] + return await self.organizations_repository.save(organization) + + async def find_by_slug(self, slug: str) -> Organization: + return await self.organizations_repository.find_by_slug(slug) diff --git a/amt/services/users.py b/amt/services/users.py index 921d082e..dd1a7411 100644 --- a/amt/services/users.py +++ b/amt/services/users.py @@ -23,3 +23,6 @@ async def get(self, id: str | UUID) -> User | None: async def create_or_update(self, user: User) -> User: return await self.repository.upsert(user) + + async def find_by_id(self, id: UUID | str) -> User | None: + return await self.repository.find_by_id(id) diff --git a/amt/site/static/scss/layout.scss b/amt/site/static/scss/layout.scss index 17f0733d..66003a24 100644 --- a/amt/site/static/scss/layout.scss +++ b/amt/site/static/scss/layout.scss @@ -319,8 +319,6 @@ main { } .amt-item-list__item_as_select { - padding-left: 1em; - &:hover { background-color: var(--rvo-color-logoblauw-150); } @@ -390,4 +388,9 @@ main { .amt-tooltip:hover .amt-tooltip__text { visibility: visible; } + /* stylelint-enable */ + +.amt-cursor-pointer { + cursor: pointer; +} diff --git a/amt/site/static/ts/amt.ts b/amt/site/static/ts/amt.ts index 2885ec71..267a6fe6 100644 --- a/amt/site/static/ts/amt.ts +++ b/amt/site/static/ts/amt.ts @@ -108,6 +108,32 @@ export function openModal(id: string) { } } +export function openConfirmModal( + title: string, + content: string, + action_type: string, + action_url: string, +) { + const template: HTMLElement | null = document.getElementById( + "confirm-modal-template", + ); + if (template) { + const clone: Node = (template as HTMLTemplateElement).content.cloneNode( + true, + ); + (clone as HTMLElement).querySelector("[data-target='title']")!.innerHTML = + title; + (clone as HTMLElement).querySelector("[data-target='content']")!.innerHTML = + content; + (clone as HTMLElement) + .querySelector("[data-target='confirm-button']")! + .setAttribute(action_type, action_url); + document.body.appendChild(clone); + // TODO: find out why process does not work on the clone element and if there are side effects to do it on the body + htmx.process(document.body); + } +} + class AiActProfile { constructor( public type: string[], @@ -124,6 +150,23 @@ export function closeModal(id: string) { const el: Element | null = document.getElementById(id); if (el != null) { el.classList.add("display-none"); + } else { + console.error( + "Can not close modal, element with id '" + id + "' not found", + ); + } +} + +export function closeAndResetDynamicModal(id: string) { + closeModal(id); + const el: Element | null = document.getElementById("dynamic-modal-content"); + if (el) { + // we want to remove all modal content to avoid unwanted or unexpected behaviour + el.innerHTML = ""; + } else { + console.error( + "Can not reset modal content, element with id 'dynamic-modal-content' not found", + ); } } diff --git a/amt/site/templates/macros/form_macros.html.j2 b/amt/site/templates/macros/form_macros.html.j2 index c3286f48..0857f49b 100644 --- a/amt/site/templates/macros/form_macros.html.j2 +++ b/amt/site/templates/macros/form_macros.html.j2 @@ -43,6 +43,21 @@ {{ organization.modified_at | time_ago(language) }} {% endmacro %} +{% macro overview_table_row_member(loop, member) -%} + + {{ member.name }} + + + + {% trans %}Delete member{% endtrans %} + + + +{% endmacro %} {% macro render_form_field_search_result(value, display_value) -%} {% set list_result = render_form_field_list_result("user_ids", value, display_value) %}
    • - {% if user %}{{ render_form_field_list_result("user_ids", user.sub, user.name) }}{% endif %} + {% if field.default_value %} + {{ render_form_field_list_result("user_ids", field.default_value.value, field.default_value.display_value) }} + {% endif %}
    {% endmacro %} diff --git a/amt/site/templates/macros/tabs.html.j2 b/amt/site/templates/macros/tabs.html.j2 new file mode 100644 index 00000000..303e53c3 --- /dev/null +++ b/amt/site/templates/macros/tabs.html.j2 @@ -0,0 +1,17 @@ +{% macro show_tabs(tab_items) %} +
    + +
    +{% endmacro %} diff --git a/amt/site/templates/organizations/home.html.j2 b/amt/site/templates/organizations/home.html.j2 index aa40837e..0b6f6dc5 100644 --- a/amt/site/templates/organizations/home.html.j2 +++ b/amt/site/templates/organizations/home.html.j2 @@ -1,23 +1,10 @@ {% extends 'layouts/base.html.j2' %} {% from "macros/editable.html.j2" import editable with context %} {% import "macros/form_macros.html.j2" as macros with context %} +{% import "macros/tabs.html.j2" as tabs with context %} {% block content %}
    -
    - -
    + {{ tabs.show_tabs(tab_items) }}
    diff --git a/amt/site/templates/organizations/members.html.j2 b/amt/site/templates/organizations/members.html.j2 new file mode 100644 index 00000000..34581fee --- /dev/null +++ b/amt/site/templates/organizations/members.html.j2 @@ -0,0 +1,50 @@ +{% 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 'organizations/parts/members_results.html.j2' %} +
    +
    + + +{% endblock content %} diff --git a/amt/site/templates/organizations/parts/add_members_modal.html.j2 b/amt/site/templates/organizations/parts/add_members_modal.html.j2 new file mode 100644 index 00000000..d04181dc --- /dev/null +++ b/amt/site/templates/organizations/parts/add_members_modal.html.j2 @@ -0,0 +1,29 @@ +{% import "macros/form_macros.html.j2" as macros with context %} +
    +
    +

    {% trans %}Add members{% endtrans %}

    + + +
    +
    +
    {{ macros.form_field(form.id, form.fields[2]) }}
    +

    + + +

    + +
    diff --git a/amt/site/templates/organizations/parts/members_results.html.j2 b/amt/site/templates/organizations/parts/members_results.html.j2 new file mode 100644 index 00000000..30275f86 --- /dev/null +++ b/amt/site/templates/organizations/parts/members_results.html.j2 @@ -0,0 +1,129 @@ +{% import "macros/table_row.html.j2" as table_row with context %} +{% import "macros/form_macros.html.j2" as macros with context %} +{% if include_filters %} +
    +
    +

    {% trans %}Members{% endtrans %}

    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    {% trans %}Search{% endtrans %}
    +
    + + +
    +
    +
    +
    +
    +
    +
    + +{% endif %} +{% if start > 0 %} + {% for member in members %}{{ macros.overview_table_row_member(loop, member) }}{% endfor %} +{% else %} +
    +
    +
    + + {% trans members_length %}{{ members_length }} result + {% pluralize %} + {{ members_length }} results{% endtrans %} + + {% if search or filters %} + {% trans %}for{% endtrans %} + {% if search %}'{{ search }}'{% endif %} + {% endif %} +
    + + {% for key, localized_value in filters.items() %} + + + {{ localized_value.display_value }} + + + {% endfor %} + +
    +
    +
    + + {% for key, localized_value in filters.items() %} + + {% endfor %} + {% if members|length > 0 %} +
    + + + + + + + + + + + {% for member in members %}{{ macros.overview_table_row_member(loop, member) }}{% endfor %} +
    + {% trans %}Name{% endtrans %} + {{ table_row.sort_button('name', sort_by, "/organizations/" + slug + "/members") }} + {% trans %}Action{% endtrans %}
    + {% endif %} + +
    +{% endif %} diff --git a/amt/site/templates/organizations/parts/overview_results.html.j2 b/amt/site/templates/organizations/parts/overview_results.html.j2 index aeca08a7..68a07ff2 100644 --- a/amt/site/templates/organizations/parts/overview_results.html.j2 +++ b/amt/site/templates/organizations/parts/overview_results.html.j2 @@ -15,7 +15,6 @@
    @@ -51,8 +50,11 @@
    {% trans %}Organisation Type{% endtrans %}