From edbd1f006112e0980e4b9d4f7e2b13b6d5645986 Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Fri, 22 Nov 2024 14:18:50 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(user)=20add=20organization=20data=20o?= =?UTF-8?q?n=20users=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will allow the frontend to display data about organizations when displaying a user or a list of users. --- CHANGELOG.md | 1 + src/backend/core/api/client/serializers.py | 13 +++ src/backend/core/api/client/viewsets.py | 4 +- src/backend/core/models.py | 10 +- src/backend/core/tests/test_api_users.py | 105 ++++++++++++++++++++- 5 files changed, 119 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c64564b..c79dc72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - ✨(mailbox) allow to disable mailbox - ✨(backend) add ServiceProvider #522 - 💄(admin) allow header color customization #552 +- ✨(organization) add API endpoints #551 ### Fixed diff --git a/src/backend/core/api/client/serializers.py b/src/backend/core/api/client/serializers.py index 461258d..ff46554 100644 --- a/src/backend/core/api/client/serializers.py +++ b/src/backend/core/api/client/serializers.py @@ -69,12 +69,22 @@ class OrganizationSerializer(serializers.ModelSerializer): return {} +class UserOrganizationSerializer(serializers.ModelSerializer): + """Serialize organizations for users.""" + + class Meta: + model = models.Organization + fields = ["id", "name"] + read_only_fields = ["id", "name"] + + class UserSerializer(DynamicFieldsModelSerializer): """Serialize users.""" timezone = TimeZoneSerializerField(use_pytz=False, required=True) email = serializers.ReadOnlyField() name = serializers.ReadOnlyField() + organization = UserOrganizationSerializer(read_only=True) class Meta: model = models.User @@ -83,6 +93,7 @@ class UserSerializer(DynamicFieldsModelSerializer): "email", "language", "name", + "organization", "timezone", "is_device", "is_staff", @@ -98,6 +109,7 @@ class UserMeSerializer(UserSerializer): """ abilities = serializers.SerializerMethodField() + organization = UserOrganizationSerializer(read_only=True) class Meta: model = models.User @@ -108,6 +120,7 @@ class UserMeSerializer(UserSerializer): "is_staff", "language", "name", + "organization", "timezone", # added fields "abilities", diff --git a/src/backend/core/api/client/viewsets.py b/src/backend/core/api/client/viewsets.py index a20bba9..2849f18 100644 --- a/src/backend/core/api/client/viewsets.py +++ b/src/backend/core/api/client/viewsets.py @@ -225,7 +225,9 @@ class UserViewSet( """ permission_classes = [permissions.IsSelf] - queryset = models.User.objects.all().order_by("-created_at") + queryset = ( + models.User.objects.select_related("organization").all().order_by("-created_at") + ) serializer_class = serializers.UserSerializer get_me_serializer_class = serializers.UserMeSerializer throttle_classes = [BurstRateThrottle, SustainedRateThrottle] diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 6374d56..cc99fbe 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -359,14 +359,8 @@ class Organization(BaseModel): """ Compute and return abilities for a given user on the organization. """ - try: - # Use the role from queryset annotation if available - is_admin = self.user_role == OrganizationRoleChoices.ADMIN - except AttributeError: - is_admin = self.organization_accesses.filter( - user=user, - role=OrganizationRoleChoices.ADMIN, - ).exists() + # Use the role from queryset annotation will raise on purpose if not used properly + is_admin = self.user_role == OrganizationRoleChoices.ADMIN # pylint: disable=no-member return { "get": user.organization_id == self.pk, diff --git a/src/backend/core/tests/test_api_users.py b/src/backend/core/tests/test_api_users.py index e321679..2879431 100644 --- a/src/backend/core/tests/test_api_users.py +++ b/src/backend/core/tests/test_api_users.py @@ -51,6 +51,73 @@ def test_api_users_list_authenticated(): assert len(response.json()["results"]) == 3 +def test_api_users_list_authenticated_response_content( + client, django_assert_num_queries +): + """ + Authenticated users should be able to list all users with the expected output. + """ + user_organization = factories.OrganizationFactory( + with_registration_id=True, name="HAL 9000" + ) + user = factories.UserFactory( + organization=user_organization, + email="kylefields@example.net", + name="Mr. Christopher Curtis", + language="en-us", + ) + + client.force_login(user) + + other_user_organization = factories.OrganizationFactory( + with_registration_id=True, name="Corp. Inc." + ) + other_user = factories.UserFactory( + organization=other_user_organization, + email="sara83@example.com", + name="Christopher Thompson", + language="fr-fr", + ) + + with django_assert_num_queries(3): # get User, Count, Users + response = client.get("/api/v1.0/users/") + + assert response.status_code == HTTP_200_OK + assert response.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "email": "sara83@example.com", + "id": str(other_user.pk), + "is_device": False, + "is_staff": False, + "language": "fr-fr", + "name": "Christopher Thompson", + "organization": { + "id": str(other_user.organization.pk), + "name": "Corp. Inc.", + }, + "timezone": "UTC", + }, + { + "email": "kylefields@example.net", + "id": str(user.pk), + "is_device": False, + "is_staff": False, + "language": "en-us", + "name": "Mr. Christopher Curtis", + "organization": { + "id": str(user.organization.pk), + "name": "HAL 9000", + }, + "timezone": "UTC", + }, + ], + } + + def test_api_users_authenticated_list_by_email(): """ Authenticated users should be able to search users with a case-insensitive and @@ -58,8 +125,12 @@ def test_api_users_authenticated_list_by_email(): """ user = factories.UserFactory(email="tester@ministry.fr", name="john doe") dave = factories.UserFactory(email="david.bowman@work.com", name=None) - nicole = factories.UserFactory(email="nicole_foole@work.com", name=None) - frank = factories.UserFactory(email="frank_poole@work.com", name=None) + nicole = factories.UserFactory( + email="nicole_foole@work.com", name=None, with_organization=True + ) + frank = factories.UserFactory( + email="frank_poole@work.com", name=None, with_organization=True + ) factories.UserFactory(email="heywood_floyd@work.com", name=None) client = APIClient() @@ -99,6 +170,10 @@ def test_api_users_authenticated_list_by_email(): "is_staff": frank.is_staff, "language": frank.language, "timezone": str(frank.timezone), + "organization": { + "id": str(frank.organization.pk), + "name": frank.organization.name, + }, }, { "id": str(nicole.id), @@ -108,6 +183,10 @@ def test_api_users_authenticated_list_by_email(): "is_staff": nicole.is_staff, "language": nicole.language, "timezone": str(nicole.timezone), + "organization": { + "id": str(nicole.organization.pk), + "name": nicole.organization.name, + }, }, ] @@ -119,8 +198,12 @@ def test_api_users_authenticated_list_by_name(): """ user = factories.UserFactory(email="tester@ministry.fr", name="john doe") dave = factories.UserFactory(name="Dave bowman", email=None) - nicole = factories.UserFactory(name="nicole foole", email=None) - frank = factories.UserFactory(name="frank poolé", email=None) + nicole = factories.UserFactory( + name="nicole foole", email=None, with_organization=True + ) + frank = factories.UserFactory( + name="frank poolé", email=None, with_organization=True + ) factories.UserFactory(name="heywood floyd", email=None) client = APIClient() @@ -160,6 +243,10 @@ def test_api_users_authenticated_list_by_name(): "is_staff": frank.is_staff, "language": frank.language, "timezone": str(frank.timezone), + "organization": { + "id": str(frank.organization.pk), + "name": frank.organization.name, + }, }, { "id": str(nicole.id), @@ -169,6 +256,10 @@ def test_api_users_authenticated_list_by_name(): "is_staff": nicole.is_staff, "language": nicole.language, "timezone": str(nicole.timezone), + "organization": { + "id": str(nicole.organization.pk), + "name": nicole.organization.name, + }, }, ] @@ -437,7 +528,7 @@ def test_api_users_retrieve_me_anonymous(): def test_api_users_retrieve_me_authenticated(): """Authenticated users should be able to retrieve their own user via the "/users/me" path.""" - user = factories.UserFactory() + user = factories.UserFactory(with_organization=True) client = APIClient() client.force_login(user) @@ -466,6 +557,10 @@ def test_api_users_retrieve_me_authenticated(): "mailboxes": {"can_create": False, "can_view": False}, "teams": {"can_create": False, "can_view": False}, }, + "organization": { + "id": str(user.organization.pk), + "name": user.organization.name, + }, }