diff --git a/CHANGELOG.md b/CHANGELOG.md index c91628a..6553d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to ### Added +- ✨(contacts) add "abilities" to API endpoint data #585 - ✨(contacts) allow filter list API with email - ✨(contacts) return profile contact from same organization - ✨(dimail) automate allows requests to dimail diff --git a/src/backend/core/api/client/serializers.py b/src/backend/core/api/client/serializers.py index 010a51f..9236b78 100644 --- a/src/backend/core/api/client/serializers.py +++ b/src/backend/core/api/client/serializers.py @@ -10,10 +10,13 @@ from core.models import ServiceProvider class ContactSerializer(serializers.ModelSerializer): """Serialize contacts.""" + abilities = serializers.SerializerMethodField() + class Meta: model = models.Contact fields = [ "id", + "abilities", "override", "data", "full_name", @@ -21,7 +24,7 @@ class ContactSerializer(serializers.ModelSerializer): "owner", "short_name", ] - read_only_fields = ["id", "owner"] + read_only_fields = ["id", "owner", "abilities"] extra_kwargs = { "override": {"required": False}, } @@ -31,6 +34,13 @@ class ContactSerializer(serializers.ModelSerializer): validated_data.pop("override", None) return super().update(instance, validated_data) + def get_abilities(self, contact) -> dict: + """Return abilities of the logged-in user on the instance.""" + request = self.context.get("request") + if request: + return contact.get_abilities(request.user) + return {} + class DynamicFieldsModelSerializer(serializers.ModelSerializer): """ diff --git a/src/backend/core/api/client/viewsets.py b/src/backend/core/api/client/viewsets.py index a6a6f01..50167bf 100644 --- a/src/backend/core/api/client/viewsets.py +++ b/src/backend/core/api/client/viewsets.py @@ -137,7 +137,7 @@ class ContactViewSet( """Contact ViewSet""" permission_classes = [permissions.AccessPermission] - queryset = models.Contact.objects.all() + queryset = models.Contact.objects.select_related("user").all() serializer_class = serializers.ContactSerializer throttle_classes = [BurstRateThrottle, SustainedRateThrottle] ordering_fields = ["full_name", "short_name", "created_at"] diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 730bf11..0e81ac8 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -196,8 +196,8 @@ class Contact(BaseModel): For now, we still consider here, a contact without owner is "public" so we allow access to it. """ - is_owner = user == self.owner - is_profile_member_or_same_organization = bool(self.user) and ( + is_owner = user.pk == self.owner_id + is_profile_member_or_same_organization = bool(self.user_id) and ( self.user.organization_id == user.organization_id ) diff --git a/src/backend/core/tests/contacts/test_core_api_contacts_create.py b/src/backend/core/tests/contacts/test_core_api_contacts_create.py index b2568a7..c86fd0c 100644 --- a/src/backend/core/tests/contacts/test_core_api_contacts_create.py +++ b/src/backend/core/tests/contacts/test_core_api_contacts_create.py @@ -86,6 +86,7 @@ def test_api_contacts_create_authenticated_missing_base(): assert response.json() == { "override": None, + "abilities": {"delete": True, "get": True, "patch": True, "put": True}, "data": {}, "full_name": "David Bowman", "id": str(new_contact.pk), @@ -123,6 +124,7 @@ def test_api_contacts_create_authenticated_successful(): contact = models.Contact.objects.get(owner=user) assert response.json() == { "id": str(contact.id), + "abilities": {"delete": True, "get": True, "patch": True, "put": True}, "override": str(base_contact.id), "data": CONTACT_DATA, "full_name": "David Bowman", @@ -195,6 +197,7 @@ def test_api_contacts_create_authenticated_successful_with_notes(): contact = models.Contact.objects.get(owner=user) assert response.json() == { "id": str(contact.id), + "abilities": {"delete": True, "get": True, "patch": True, "put": True}, "override": None, "data": CONTACT_DATA, "full_name": "David Bowman", diff --git a/src/backend/core/tests/contacts/test_core_api_contacts_list.py b/src/backend/core/tests/contacts/test_core_api_contacts_list.py index 3613861..2ad299c 100644 --- a/src/backend/core/tests/contacts/test_core_api_contacts_list.py +++ b/src/backend/core/tests/contacts/test_core_api_contacts_list.py @@ -22,7 +22,7 @@ def test_api_contacts_list_anonymous(): } -def test_api_contacts_list_authenticated_no_query(): +def test_api_contacts_list_authenticated_no_query(django_assert_num_queries): """ Authenticated users should be able to list contacts without applying a query. Profile and overridden contacts should be excluded. @@ -57,12 +57,14 @@ def test_api_contacts_list_authenticated_no_query(): client = APIClient() client.force_login(user) - response = client.get("/api/v1.0/contacts/") + with django_assert_num_queries(2): + response = client.get("/api/v1.0/contacts/") assert response.status_code == 200 assert response.json() == [ { "id": str(user_profile_contact.pk), + "abilities": {"delete": False, "get": True, "patch": True, "put": True}, "override": None, "owner": str(user.pk), "data": user_profile_contact.data, @@ -72,6 +74,7 @@ def test_api_contacts_list_authenticated_no_query(): }, { "id": str(profile_contact.pk), + "abilities": {"delete": False, "get": True, "patch": False, "put": False}, "override": None, "owner": str(profile_contact.user.pk), "data": profile_contact.data, @@ -81,6 +84,7 @@ def test_api_contacts_list_authenticated_no_query(): }, { "id": str(override_contact.pk), + "abilities": {"delete": True, "get": True, "patch": True, "put": True}, "override": str(overriden_contact.pk), "owner": str(user.pk), "data": override_contact.data, diff --git a/src/backend/core/tests/contacts/test_core_api_contacts_retrieve.py b/src/backend/core/tests/contacts/test_core_api_contacts_retrieve.py index 0f14a0e..846588a 100644 --- a/src/backend/core/tests/contacts/test_core_api_contacts_retrieve.py +++ b/src/backend/core/tests/contacts/test_core_api_contacts_retrieve.py @@ -22,7 +22,7 @@ def test_api_contacts_retrieve_anonymous(): } -def test_api_contacts_retrieve_authenticated_owned(): +def test_api_contacts_retrieve_authenticated_owned(django_assert_num_queries): """ Authenticated users should be allowed to retrieve a contact they own. """ @@ -32,11 +32,13 @@ def test_api_contacts_retrieve_authenticated_owned(): client = APIClient() client.force_login(user) - response = client.get(f"/api/v1.0/contacts/{contact.id!s}/") + with django_assert_num_queries(2): + response = client.get(f"/api/v1.0/contacts/{contact.id!s}/") assert response.status_code == 200 assert response.json() == { "id": str(contact.id), + "abilities": {"delete": True, "get": True, "patch": True, "put": True}, "override": None, "owner": str(contact.owner.id), "data": contact.data, @@ -60,6 +62,7 @@ def test_api_contacts_retrieve_authenticated_public(): assert response.status_code == 200 assert response.json() == { "id": str(contact.id), + "abilities": {"delete": False, "get": True, "patch": False, "put": False}, "override": None, "owner": None, "data": contact.data, diff --git a/src/backend/core/tests/contacts/test_core_api_contacts_update.py b/src/backend/core/tests/contacts/test_core_api_contacts_update.py index 834e325..8383a8f 100644 --- a/src/backend/core/tests/contacts/test_core_api_contacts_update.py +++ b/src/backend/core/tests/contacts/test_core_api_contacts_update.py @@ -35,7 +35,7 @@ def test_api_contacts_update_anonymous(): assert contact_values == old_contact_values -def test_api_contacts_update_authenticated_owned(): +def test_api_contacts_update_authenticated_owned(django_assert_num_queries): """ Authenticated users should be allowed to update their own contacts. """ @@ -52,11 +52,13 @@ def test_api_contacts_update_authenticated_owned(): ).data new_contact_values["override"] = str(factories.ContactFactory().id) - response = client.put( - f"/api/v1.0/contacts/{contact.id!s}/", - new_contact_values, - format="json", - ) + with django_assert_num_queries(8): + # user, 2x contact, user, 3x check, update contact + response = client.put( + f"/api/v1.0/contacts/{contact.id!s}/", + new_contact_values, + format="json", + ) assert response.status_code == 200