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 f6be2508..f86357c1 100644 Binary files a/amt/locale/nl_NL/LC_MESSAGES/messages.mo and b/amt/locale/nl_NL/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.po b/amt/locale/nl_NL/LC_MESSAGES/messages.po index 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 @@