From d81235198ecf1640200bbcce658020cb61f1afd0 Mon Sep 17 00:00:00 2001 From: prafful Date: Wed, 6 Nov 2024 16:39:22 +0530 Subject: [PATCH 1/9] added user/facility flag crud endpoint and their tests --- .../facility/api/serializers/facility_flag.py | 13 ++ care/facility/api/viewsets/facility_flag.py | 12 ++ .../facility/tests/test_facility_flags_api.py | 127 ++++++++++++++++++ care/facility/tests/test_user_flags_api.py | 120 +++++++++++++++++ care/users/api/serializers/user_flag.py | 13 ++ care/users/api/viewsets/user_flag.py | 12 ++ care/utils/custom_permissions.py | 6 + care/utils/tests/test_utils.py | 23 +++- 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 care/facility/api/serializers/facility_flag.py create mode 100644 care/facility/api/viewsets/facility_flag.py create mode 100644 care/facility/tests/test_facility_flags_api.py create mode 100644 care/facility/tests/test_user_flags_api.py create mode 100644 care/users/api/serializers/user_flag.py create mode 100644 care/users/api/viewsets/user_flag.py create mode 100644 care/utils/custom_permissions.py diff --git a/care/facility/api/serializers/facility_flag.py b/care/facility/api/serializers/facility_flag.py new file mode 100644 index 0000000000..8ef72da822 --- /dev/null +++ b/care/facility/api/serializers/facility_flag.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from care.facility.models import Facility, FacilityFlag +from care.utils.serializers.fields import ExternalIdSerializerField + + +class FacilityFlagSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + facility = ExternalIdSerializerField(queryset=Facility.objects.all(), required=True) + + class Meta: + model = FacilityFlag + exclude = ["external_id", "deleted", "modified_date"] diff --git a/care/facility/api/viewsets/facility_flag.py b/care/facility/api/viewsets/facility_flag.py new file mode 100644 index 0000000000..8c16e1aea3 --- /dev/null +++ b/care/facility/api/viewsets/facility_flag.py @@ -0,0 +1,12 @@ +from rest_framework import viewsets + +from care.facility.api.serializers.facility_flag import FacilityFlagSerializer +from care.facility.models import FacilityFlag +from care.utils.custom_permissions import IsSuperUser + + +class FacilityFlagViewSet(viewsets.ModelViewSet): + queryset = FacilityFlag.objects.all() + serializer_class = FacilityFlagSerializer + permission_classes = [IsSuperUser] + lookup_field = "external_id" diff --git a/care/facility/tests/test_facility_flags_api.py b/care/facility/tests/test_facility_flags_api.py new file mode 100644 index 0000000000..17a4b22c31 --- /dev/null +++ b/care/facility/tests/test_facility_flags_api.py @@ -0,0 +1,127 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.utils.registries.feature_flag import FlagRegistry, FlagType +from care.utils.tests.test_utils import TestUtils + + +class FacilityFlagsViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + FlagRegistry.register(FlagType.FACILITY, "TEST_FLAG") + FlagRegistry.register(FlagType.FACILITY, "TEST_FLAG_2") + + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.facility2 = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.user = cls.create_user("staff", cls.district, home_facility=cls.facility) + cls.facility_flag_2 = cls.create_facility_flag("TEST_FLAG_2", cls.facility) + + def setUp(self): + self.facility_flag_1 = self.create_facility_flag("TEST_FLAG", self.facility) + + def get_url(self, facility_flag_id=None): + base_url = "/api/v1/facility_flags/" + if facility_flag_id is not None: + base_url += f"{facility_flag_id}/" + return base_url + + def test_access_with_non_super_user(self): + self.client.force_authenticate(user=self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_access_with_super_user(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_list_facility_flags(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["count"], 2) + + def test_create_facility_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Attempting to create a duplicate flag + response = self.client.post( + self.get_url(), {"flag": "TEST_FLAG", "facility": self.facility.external_id} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # Creating a new facility flag + response = self.client.post( + self.get_url(), + {"flag": "TEST_FLAG", "facility": self.facility2.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_retrieve_facility_flag(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url(self.facility_flag_1.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["flag"], "TEST_FLAG") + self.assertEqual(data["facility"], str(self.facility.external_id)) + + def test_update_facility_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Confirming original values + response = self.client.get(self.get_url(self.facility_flag_1.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["flag"], "TEST_FLAG") + self.assertEqual(data["facility"], str(self.facility.external_id)) + + # Update the facility flag + response = self.client.put( + self.get_url(self.facility_flag_1.external_id), + {"flag": "TEST_FLAG", "facility": self.facility2.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.facility_flag_1.refresh_from_db() + self.assertEqual(self.facility_flag_1.flag, "TEST_FLAG") + self.assertEqual( + self.facility_flag_1.facility.external_id, self.facility2.external_id + ) + + def test_patch_facility_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Confirming original values + response = self.client.get(self.get_url(self.facility_flag_1.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["flag"], "TEST_FLAG") + self.assertEqual(data["facility"], str(self.facility.external_id)) + + # Patch the facility flag + response = self.client.patch( + self.get_url(self.facility_flag_1.external_id), + {"facility": self.facility2.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.facility_flag_1.refresh_from_db() + self.assertEqual(self.facility_flag_1.flag, "TEST_FLAG") + self.assertEqual( + self.facility_flag_1.facility.external_id, self.facility2.external_id + ) + + def test_creating_facility_flag_with_non_existing_flag(self): + self.client.force_authenticate(user=self.super_user) + + response = self.client.post( + self.get_url(), + {"flag": "TEST_FLAG_NON_EXISTING", "facility": self.facility2.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["detail"], "Flag not registered") diff --git a/care/facility/tests/test_user_flags_api.py b/care/facility/tests/test_user_flags_api.py new file mode 100644 index 0000000000..e943719783 --- /dev/null +++ b/care/facility/tests/test_user_flags_api.py @@ -0,0 +1,120 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.utils.registries.feature_flag import FlagRegistry, FlagType +from care.utils.tests.test_utils import TestUtils + + +class UserFlagsViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + FlagRegistry.register(FlagType.USER, "TEST_FLAG_2") + + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user("staff", cls.district, home_facility=cls.facility) + cls.user_2 = cls.create_user("user2", cls.district, home_facility=cls.facility) + cls.user_flag_2 = cls.create_user_flag("TEST_FLAG_2", cls.user_2) + + def setUp(self): + self.user_flag_1 = self.create_user_flag("TEST_FLAG", self.user) + + def get_url(self, user_flag_id=None): + base_url = "/api/v1/user_flags/" + if user_flag_id is not None: + base_url += f"{user_flag_id}/" + return base_url + + def test_access_with_non_super_user(self): + self.client.force_authenticate(user=self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_access_with_super_user(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_list_user_flags(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["count"], 2) + + def test_create_user_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Attempting to create a duplicate flag + response = self.client.post( + self.get_url(), {"flag": "TEST_FLAG", "user": self.user.external_id} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # Creating a new user flag + response = self.client.post( + self.get_url(), {"flag": "TEST_FLAG", "user": self.user_2.external_id} + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_retrieve_user_flag(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url(self.user_flag_1.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["flag"], "TEST_FLAG") + self.assertEqual(data["user"], str(self.user.external_id)) + + def test_update_user_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Confirm original values + response = self.client.get(self.get_url(self.user_flag_1.external_id)) + data = response.json() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(data["flag"], "TEST_FLAG") + self.assertEqual(data["user"], str(self.user.external_id)) + + # Update the user flag + response = self.client.put( + self.get_url(self.user_flag_1.external_id), + {"flag": "TEST_FLAG", "user": self.user_2.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user_flag_1.refresh_from_db() + self.assertEqual(self.user_flag_1.flag, "TEST_FLAG") + self.assertEqual(self.user_flag_1.user.external_id, self.user_2.external_id) + + def test_patch_user_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Confirm original values + response = self.client.get(self.get_url(self.user_flag_1.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["flag"], "TEST_FLAG") + self.assertEqual(data["user"], str(self.user.external_id)) + + # Patch the user flag + response = self.client.patch( + self.get_url(self.user_flag_1.external_id), + {"user": self.user_2.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user_flag_1.refresh_from_db() + self.assertEqual(self.user_flag_1.flag, "TEST_FLAG") + self.assertEqual(self.user_flag_1.user.external_id, self.user_2.external_id) + + def test_creating_user_flag_with_non_existing_flag(self): + self.client.force_authenticate(user=self.super_user) + + response = self.client.post( + self.get_url(), + {"flag": "TEST_FLAG_NON_EXISTING", "user": self.user.external_id}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["detail"], "Flag not registered") diff --git a/care/users/api/serializers/user_flag.py b/care/users/api/serializers/user_flag.py new file mode 100644 index 0000000000..6c2d43eb6c --- /dev/null +++ b/care/users/api/serializers/user_flag.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from care.users.models import User, UserFlag +from care.utils.serializers.fields import ExternalIdSerializerField + + +class UserFlagSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + user = ExternalIdSerializerField(queryset=User.objects.all(), required=True) + + class Meta: + model = UserFlag + exclude = ["external_id", "deleted", "modified_date"] diff --git a/care/users/api/viewsets/user_flag.py b/care/users/api/viewsets/user_flag.py new file mode 100644 index 0000000000..815e4be29d --- /dev/null +++ b/care/users/api/viewsets/user_flag.py @@ -0,0 +1,12 @@ +from rest_framework import viewsets + +from care.users.api.serializers.user_flag import UserFlagSerializer +from care.users.models import UserFlag +from care.utils.custom_permissions import IsSuperUser + + +class UserFlagViewSet(viewsets.ModelViewSet): + queryset = UserFlag.objects.all() + serializer_class = UserFlagSerializer + permission_classes = [IsSuperUser] + lookup_field = "external_id" diff --git a/care/utils/custom_permissions.py b/care/utils/custom_permissions.py new file mode 100644 index 0000000000..9ca4ab21db --- /dev/null +++ b/care/utils/custom_permissions.py @@ -0,0 +1,6 @@ +from rest_framework.permissions import BasePermission + + +class IsSuperUser(BasePermission): + def has_permission(self, request, view): + return request.user and request.user.is_superuser diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 91d4ac8d67..1622a0cddc 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -22,6 +22,7 @@ DiseaseStatusEnum, EncounterSymptom, Facility, + FacilityFlag, InvestigationSession, InvestigationValue, LocalBody, @@ -51,7 +52,7 @@ PatientCodeStatusType, PatientConsent, ) -from care.users.models import District, State +from care.users.models import District, State, UserFlag fake = Faker() @@ -728,6 +729,26 @@ def create_prescription( data.update(**kwargs) return Prescription.objects.create(**data) + @classmethod + def create_facility_flag( + cls, flag: str, facility: Facility, **kwargs + ) -> FacilityFlag: + data = { + "facility": facility, + "flag": flag, + } + data.update(**kwargs) + return FacilityFlag.objects.create(**data) + + @classmethod + def create_user_flag(cls, flag: str, user: User, **kwargs) -> UserFlag: + data = { + "user": user, + "flag": flag, + } + data.update(**kwargs) + return UserFlag.objects.create(**data) + def get_list_representation(self, obj) -> dict: """ Returns the dict representation of the obj in list API From 1e07d95dbb4496d46fddfe743fb38fecbd8784ee Mon Sep 17 00:00:00 2001 From: prafful Date: Wed, 6 Nov 2024 16:41:18 +0530 Subject: [PATCH 2/9] moved test to correct folder --- care/{facility => users}/tests/test_user_flags_api.py | 0 config/api_router.py | 5 +++++ 2 files changed, 5 insertions(+) rename care/{facility => users}/tests/test_user_flags_api.py (100%) diff --git a/care/facility/tests/test_user_flags_api.py b/care/users/tests/test_user_flags_api.py similarity index 100% rename from care/facility/tests/test_user_flags_api.py rename to care/users/tests/test_user_flags_api.py diff --git a/config/api_router.py b/config/api_router.py index 94b18f61de..0a821fd1f6 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -40,6 +40,7 @@ FacilityViewSet, ) from care.facility.api.viewsets.facility_capacity import FacilityCapacityViewSet +from care.facility.api.viewsets.facility_flag import FacilityFlagViewSet from care.facility.api.viewsets.facility_users import FacilityUserViewSet from care.facility.api.viewsets.file_upload import FileUploadViewSet from care.facility.api.viewsets.hospital_doctor import HospitalDoctorViewSet @@ -101,6 +102,7 @@ ) from care.users.api.viewsets.plug_config import PlugConfigViewset from care.users.api.viewsets.skill import SkillViewSet +from care.users.api.viewsets.user_flag import UserFlagViewSet from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet @@ -315,6 +317,9 @@ router.register("medibase", MedibaseViewSet, basename="medibase") +router.register(r"facility_flags", FacilityFlagViewSet, basename="facility-flags") +router.register(r"user_flags", UserFlagViewSet, basename="user-flags") + # Public endpoints router.register("public/asset", AssetPublicViewSet, basename="public-asset") router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") From 34c97d08ded2347af3e5a7662a2e917e109691e9 Mon Sep 17 00:00:00 2001 From: prafful Date: Wed, 6 Nov 2024 16:45:02 +0530 Subject: [PATCH 3/9] excluded create at field --- care/facility/api/serializers/facility_flag.py | 2 +- care/users/api/serializers/user_flag.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/care/facility/api/serializers/facility_flag.py b/care/facility/api/serializers/facility_flag.py index 8ef72da822..ad8647e9b9 100644 --- a/care/facility/api/serializers/facility_flag.py +++ b/care/facility/api/serializers/facility_flag.py @@ -10,4 +10,4 @@ class FacilityFlagSerializer(serializers.ModelSerializer): class Meta: model = FacilityFlag - exclude = ["external_id", "deleted", "modified_date"] + exclude = ["external_id", "deleted", "modified_date", "created_date"] diff --git a/care/users/api/serializers/user_flag.py b/care/users/api/serializers/user_flag.py index 6c2d43eb6c..6510c5c6a0 100644 --- a/care/users/api/serializers/user_flag.py +++ b/care/users/api/serializers/user_flag.py @@ -10,4 +10,4 @@ class UserFlagSerializer(serializers.ModelSerializer): class Meta: model = UserFlag - exclude = ["external_id", "deleted", "modified_date"] + exclude = ["external_id", "deleted", "modified_date", "created_date"] From bf7a237b717f637810719e11da70771d4e6d4240 Mon Sep 17 00:00:00 2001 From: prafful Date: Wed, 6 Nov 2024 18:04:11 +0530 Subject: [PATCH 4/9] added filter and improved tests --- care/facility/api/viewsets/facility_flag.py | 15 ++++++++++ .../facility/tests/test_facility_flags_api.py | 29 +++++++++++++++++++ care/users/api/viewsets/user_flag.py | 15 ++++++++++ care/users/tests/test_user_flags_api.py | 26 +++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/care/facility/api/viewsets/facility_flag.py b/care/facility/api/viewsets/facility_flag.py index 8c16e1aea3..b3e18e5365 100644 --- a/care/facility/api/viewsets/facility_flag.py +++ b/care/facility/api/viewsets/facility_flag.py @@ -1,3 +1,4 @@ +from django_filters import rest_framework as filters from rest_framework import viewsets from care.facility.api.serializers.facility_flag import FacilityFlagSerializer @@ -5,8 +6,22 @@ from care.utils.custom_permissions import IsSuperUser +class FacilityFlagFilter(filters.FilterSet): + flag = filters.CharFilter(field_name="flag", lookup_expr="icontains") + facility = filters.UUIDFilter(field_name="facility__external_id") + + class FacilityFlagViewSet(viewsets.ModelViewSet): + """ + CRUD operations for FacilityFlag model. + + This viewset is restricted to superusers only and provides endpoints to manage facility flags. + """ + queryset = FacilityFlag.objects.all() serializer_class = FacilityFlagSerializer permission_classes = [IsSuperUser] lookup_field = "external_id" + + filter_backends = [filters.DjangoFilterBackend] + filterset_class = FacilityFlagFilter diff --git a/care/facility/tests/test_facility_flags_api.py b/care/facility/tests/test_facility_flags_api.py index 17a4b22c31..2cf0884b9b 100644 --- a/care/facility/tests/test_facility_flags_api.py +++ b/care/facility/tests/test_facility_flags_api.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase +from care.facility.models import FacilityFlag from care.utils.registries.feature_flag import FlagRegistry, FlagType from care.utils.tests.test_utils import TestUtils @@ -51,11 +52,20 @@ def test_list_facility_flags(self): def test_create_facility_flag(self): self.client.force_authenticate(user=self.super_user) + response = self.client.post(self.get_url(), {}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("This field is required.", response.json()["flag"]) + self.assertIn("This field is required.", response.json()["facility"]) + # Attempting to create a duplicate flag response = self.client.post( self.get_url(), {"flag": "TEST_FLAG", "facility": self.facility.external_id} ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "The fields facility, flag must make a unique set.", + response.json()["non_field_errors"], + ) # Creating a new facility flag response = self.client.post( @@ -116,6 +126,25 @@ def test_patch_facility_flag(self): self.facility_flag_1.facility.external_id, self.facility2.external_id ) + def test_delete_facility_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Confirm if the object exist + self.assertTrue( + FacilityFlag.objects.filter( + external_id=self.facility_flag_1.external_id + ).exists() + ) + response = self.client.delete(self.get_url(self.facility_flag_1.external_id)) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + self.facility_flag_1.refresh_from_db() + self.assertFalse( + FacilityFlag.objects.filter( + external_id=self.facility_flag_1.external_id + ).exists() + ) + def test_creating_facility_flag_with_non_existing_flag(self): self.client.force_authenticate(user=self.super_user) diff --git a/care/users/api/viewsets/user_flag.py b/care/users/api/viewsets/user_flag.py index 815e4be29d..bce2ad0c3b 100644 --- a/care/users/api/viewsets/user_flag.py +++ b/care/users/api/viewsets/user_flag.py @@ -1,3 +1,4 @@ +from django_filters import rest_framework as filters from rest_framework import viewsets from care.users.api.serializers.user_flag import UserFlagSerializer @@ -5,8 +6,22 @@ from care.utils.custom_permissions import IsSuperUser +class UserFlagFilter(filters.FilterSet): + flag = filters.CharFilter(field_name="flag", lookup_expr="icontains") + user = filters.UUIDFilter(field_name="user__external_id") + + class UserFlagViewSet(viewsets.ModelViewSet): + """ + CRUD operations for UserFlag model. + + This viewset is restricted to superusers only and provides endpoints to manage user flags. + """ + queryset = UserFlag.objects.all() serializer_class = UserFlagSerializer permission_classes = [IsSuperUser] lookup_field = "external_id" + + filter_backends = [filters.DjangoFilterBackend] + filterset_class = UserFlagFilter diff --git a/care/users/tests/test_user_flags_api.py b/care/users/tests/test_user_flags_api.py index e943719783..0a2a02321c 100644 --- a/care/users/tests/test_user_flags_api.py +++ b/care/users/tests/test_user_flags_api.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase +from care.users.models import UserFlag from care.utils.registries.feature_flag import FlagRegistry, FlagType from care.utils.tests.test_utils import TestUtils @@ -49,11 +50,20 @@ def test_list_user_flags(self): def test_create_user_flag(self): self.client.force_authenticate(user=self.super_user) + response = self.client.post(self.get_url(), {}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("This field is required.", response.json()["flag"]) + self.assertIn("This field is required.", response.json()["user"]) + # Attempting to create a duplicate flag response = self.client.post( self.get_url(), {"flag": "TEST_FLAG", "user": self.user.external_id} ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "The fields user, flag must make a unique set.", + response.json()["non_field_errors"], + ) # Creating a new user flag response = self.client.post( @@ -109,6 +119,22 @@ def test_patch_user_flag(self): self.assertEqual(self.user_flag_1.flag, "TEST_FLAG") self.assertEqual(self.user_flag_1.user.external_id, self.user_2.external_id) + def test_delete_user_flag(self): + self.client.force_authenticate(user=self.super_user) + + # Confirm if the object exist + self.assertTrue( + UserFlag.objects.filter(external_id=self.user_flag_1.external_id).exists() + ) + + response = self.client.delete(self.get_url(self.user_flag_1.external_id)) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + self.user_flag_1.refresh_from_db() + self.assertFalse( + UserFlag.objects.filter(external_id=self.user_flag_1.external_id).exists() + ) + def test_creating_user_flag_with_non_existing_flag(self): self.client.force_authenticate(user=self.super_user) From d274955d48950b31b06c5dd8af8e5f5b662b36da Mon Sep 17 00:00:00 2001 From: prafful Date: Wed, 6 Nov 2024 18:25:15 +0530 Subject: [PATCH 5/9] updated isSuperUser permission class --- care/utils/custom_permissions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/care/utils/custom_permissions.py b/care/utils/custom_permissions.py index 9ca4ab21db..42841e4864 100644 --- a/care/utils/custom_permissions.py +++ b/care/utils/custom_permissions.py @@ -3,4 +3,9 @@ class IsSuperUser(BasePermission): def has_permission(self, request, view): - return request.user and request.user.is_superuser + return bool( + request.user + and request.user.is_authenticated + and request.user.is_active + and request.user.is_superuser + ) From ad8df9b84b92800c004b0c45246385dbe23e4f60 Mon Sep 17 00:00:00 2001 From: prafful Date: Sat, 23 Nov 2024 19:53:44 +0530 Subject: [PATCH 6/9] resolved merge conflicts --- care/facility/api/serializers/asset.py | 9 +- care/facility/api/serializers/bed.py | 12 +- care/facility/api/viewsets/asset.py | 6 +- care/facility/api/viewsets/patient.py | 2 +- care/facility/tasks/asset_monitor.py | 2 +- care/facility/tests/test_asset_api.py | 162 ++++++ care/facility/tests/test_asset_bed_api.py | 219 -------- .../facility/tests/test_asset_location_api.py | 6 +- care/facility/tests/test_assetbed_api.py | 531 ++++++++++++++++++ care/facility/tests/test_bed_api.py | 17 +- care/facility/tests/test_patient_api.py | 164 +++++- care/utils/assetintegration/base.py | 38 +- care/utils/assetintegration/hl7monitor.py | 10 +- care/utils/assetintegration/onvif.py | 20 +- care/utils/assetintegration/schema.py | 34 ++ care/utils/assetintegration/ventilator.py | 10 +- data/dummy/facility.json | 36 +- data/dummy/users.json | 6 +- 18 files changed, 1000 insertions(+), 284 deletions(-) delete mode 100644 care/facility/tests/test_asset_bed_api.py create mode 100644 care/facility/tests/test_assetbed_api.py create mode 100644 care/utils/assetintegration/schema.py diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py index f403361e1a..ddb6a8e5d4 100644 --- a/care/facility/api/serializers/asset.py +++ b/care/facility/api/serializers/asset.py @@ -7,7 +7,7 @@ from django.utils.timezone import now from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.serializers import ( CharField, JSONField, @@ -174,11 +174,14 @@ def validate(self, attrs): facilities = get_facility_queryset(user) if not facilities.filter(id=location.facility.id).exists(): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's asset." + ) + raise PermissionDenied(error_message) del attrs["location"] attrs["current_location"] = location - # validate that warraty date is not in the past + # validate that warranty date is not in the past if warranty_amc_end_of_validity := attrs.get("warranty_amc_end_of_validity"): # pop out warranty date if it is not changed if ( diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 031d2a68c1..508bc25bc4 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -2,7 +2,7 @@ from django.db.models import Exists, OuterRef, Q from django.shortcuts import get_object_or_404 from django.utils import timezone -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.serializers import ( BooleanField, CharField, @@ -74,7 +74,10 @@ def validate(self, attrs): if (not facilities.filter(id=location.facility.id).exists()) or ( not facilities.filter(id=facility.id).exists() ): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's bed." + ) + raise PermissionDenied(error_message) del attrs["location"] attrs["location"] = location attrs["facility"] = facility @@ -110,7 +113,10 @@ def validate(self, attrs): if ( not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's assetbed." + ) + raise PermissionDenied(error_message) if AssetBed.objects.filter(asset=asset, bed=bed).exists(): raise ValidationError( {"non_field_errors": "Asset is already linked to bed"} diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 8b24bebb51..2563dfaf45 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -61,6 +61,7 @@ from care.facility.models.bed import AssetBed, ConsultationBed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.base import BaseAssetIntegration from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices from care.utils.queryset.asset_bed import get_asset_queryset @@ -389,7 +390,6 @@ def operate_assets(self, request, *args, **kwargs): This API is used to operate assets. API accepts the asset_id and action as parameters. """ try: - action = request.data["action"] asset: Asset = self.get_object() middleware_hostname = ( asset.meta.get( @@ -401,11 +401,11 @@ def operate_assets(self, request, *args, **kwargs): asset_class: BaseAssetIntegration = AssetClasses[asset.asset_class].value( { **asset.meta, - "id": asset.external_id, + "id": str(asset.external_id), "middleware_hostname": middleware_hostname, } ) - result = asset_class.handle_action(action) + result = asset_class.handle_action(**request.data["action"]) return Response({"result": result}, status=status.HTTP_200_OK) except ValidationError as e: diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 72731cd6e2..963b6d4731 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -589,7 +589,7 @@ def list(self, request, *args, **kwargs): @action(detail=True, methods=["POST"]) def transfer(self, request, *args, **kwargs): patient = PatientRegistration.objects.get(external_id=kwargs["external_id"]) - facility = Facility.objects.get(external_id=request.data["facility"]) + facility = get_object_or_404(Facility, external_id=request.data["facility"]) if patient.is_expired: return Response( diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py index 9c8701618c..1adfa725ae 100644 --- a/care/facility/tasks/asset_monitor.py +++ b/care/facility/tasks/asset_monitor.py @@ -65,7 +65,7 @@ def check_asset_status(): # noqa: PLR0912 ].value( { **asset.meta, - "id": asset.external_id, + "id": str(asset.external_id), "middleware_hostname": resolved_middleware, } ) diff --git a/care/facility/tests/test_asset_api.py b/care/facility/tests/test_asset_api.py index a0771b089b..0e96498d4b 100644 --- a/care/facility/tests/test_asset_api.py +++ b/care/facility/tests/test_asset_api.py @@ -1,10 +1,15 @@ from django.utils.timezone import now, timedelta from rest_framework import status +from rest_framework.exceptions import ValidationError from rest_framework.test import APITestCase from care.facility.models import Asset, Bed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.hl7monitor import HL7MonitorAsset +from care.utils.assetintegration.onvif import OnvifAsset +from care.utils.assetintegration.ventilator import VentilatorAsset from care.utils.tests.test_utils import TestUtils @@ -31,6 +36,163 @@ def setUp(self) -> None: super().setUp() self.asset = self.create_asset(self.asset_location) + def validate_invalid_meta(self, asset_class, meta): + with self.assertRaises(ValidationError): + asset_class(meta) + + def test_asset_class_initialization(self): + asset = self.create_asset( + self.asset_location, + asset_class=AssetClasses.ONVIF.name, + meta={ + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ) + asset_class = AssetClasses[asset.asset_class].value( + { + **asset.meta, + "id": str(asset.external_id), + "middleware_hostname": "middleware.local", + } + ) + self.assertIsInstance(asset_class, BaseAssetIntegration) + + def test_meta_validations_for_onvif_asset(self): + valid_meta = { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + onvif_asset = OnvifAsset(valid_meta) + self.assertEqual(onvif_asset.middleware_hostname, "middleware.local") + self.assertEqual(onvif_asset.host, "192.168.0.1") + self.assertEqual(onvif_asset.username, "username") + self.assertEqual(onvif_asset.password, "password") + self.assertEqual(onvif_asset.access_key, "access_key") + self.assertTrue(onvif_asset.insecure_connection) + + invalid_meta_cases = [ + # Invalid format for camera_access_key + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "camera_access_key": "invalid_format", + }, + # Missing username/password in camera_access_key + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "camera_access_key": "invalid_format", + }, + # Missing middleware_hostname + { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + }, + # Missing local_ip_address + { + "middleware_hostname": "middleware.local", + "camera_access_key": "username:password:access_key", + }, + # Invalid value for insecure_connection + { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(OnvifAsset, meta) + + def test_meta_validations_for_ventilator_asset(self): + valid_meta = { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + ventilator_asset = VentilatorAsset(valid_meta) + self.assertEqual(ventilator_asset.middleware_hostname, "middleware.local") + self.assertEqual(ventilator_asset.host, "192.168.0.1") + self.assertTrue(ventilator_asset.insecure_connection) + + invalid_meta_cases = [ + # Missing id + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + }, + # Missing middleware_hostname + {"id": "123", "local_ip_address": "192.168.0.1"}, + # Missing local_ip_address + {"id": "123", "middleware_hostname": "middleware.local"}, + # Invalid insecure_connection + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + # Camera access key not required for ventilator, invalid meta + { + "id": "21", + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(VentilatorAsset, meta) + + def test_meta_validations_for_hl7monitor_asset(self): + valid_meta = { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + hl7monitor_asset = HL7MonitorAsset(valid_meta) + self.assertEqual(hl7monitor_asset.middleware_hostname, "middleware.local") + self.assertEqual(hl7monitor_asset.host, "192.168.0.1") + self.assertEqual(hl7monitor_asset.id, "123") + self.assertTrue(hl7monitor_asset.insecure_connection) + + invalid_meta_cases = [ + # Missing id + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + }, + # Missing middleware_hostname + {"id": "123", "local_ip_address": "192.168.0.1"}, + # Missing local_ip_address + {"id": "123", "middleware_hostname": "middleware.local"}, + # Invalid insecure_connection + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + # Camera access key not required for HL7Monitor, invalid meta + { + "id": "123", + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(HL7MonitorAsset, meta) + def test_list_assets(self): response = self.client.get("/api/v1/asset/") self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py deleted file mode 100644 index 4ed81a36b8..0000000000 --- a/care/facility/tests/test_asset_bed_api.py +++ /dev/null @@ -1,219 +0,0 @@ -from rest_framework import status -from rest_framework.test import APITestCase - -from care.users.models import User -from care.utils.assetintegration.asset_classes import AssetClasses -from care.utils.tests.test_utils import TestUtils - - -class AssetBedViewSetTestCase(TestUtils, APITestCase): - @classmethod - def setUpTestData(cls): - cls.state = cls.create_state() - cls.district = cls.create_district(cls.state) - cls.local_body = cls.create_local_body(cls.district) - cls.super_user = cls.create_super_user("su", cls.district) - cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) - cls.user = cls.create_user( - User.TYPE_VALUE_MAP["DistrictAdmin"], - cls.district, - home_facility=cls.facility, - ) - cls.asset_location = cls.create_asset_location(cls.facility) - cls.asset = cls.create_asset(cls.asset_location) - cls.monitor_asset_1 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name - ) - cls.monitor_asset_2 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name - ) - cls.camera_asset = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name - ) - cls.camera_asset_1 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 1" - ) - cls.camera_asset_2 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 2" - ) - cls.bed = cls.create_bed(cls.facility, cls.asset_location) - - def test_link_disallowed_asset_class_asset_to_bed(self): - data = { - "asset": self.asset.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_link_asset_to_bed_and_attempt_duplicate_linking(self): - data = { - "asset": self.camera_asset.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - # Attempt linking same camera to the same bed again. - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - # List asset beds filtered by asset and bed ID and check only 1 result exists - res = self.client.get("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertEqual(res.data["count"], 1) - - def test_linking_multiple_cameras_to_a_bed(self): - data = { - "asset": self.camera_asset_1.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - # Attempt linking another camera to same bed. - data["asset"] = self.camera_asset_2.external_id - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - - def test_linking_multiple_hl7_monitors_to_a_bed(self): - data = { - "asset": self.monitor_asset_1.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - # Attempt linking another hl7 monitor to same bed. - data["asset"] = self.monitor_asset_2.external_id - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - -class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): - @classmethod - def setUpTestData(cls): - cls.state = cls.create_state() - cls.district = cls.create_district(cls.state) - cls.local_body = cls.create_local_body(cls.district) - cls.super_user = cls.create_super_user("su", cls.district) - cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) - cls.user = cls.create_user( - User.TYPE_VALUE_MAP["DistrictAdmin"], - cls.district, - home_facility=cls.facility, - ) - cls.asset_location = cls.create_asset_location(cls.facility) - cls.asset1 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name - ) - cls.asset2 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name - ) - cls.bed = cls.create_bed(cls.facility, cls.asset_location) - cls.asset_bed1 = cls.create_asset_bed(cls.asset1, cls.bed) - cls.asset_bed2 = cls.create_asset_bed(cls.asset2, cls.bed) - - def get_base_url(self, asset_bed_id=None): - return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" - - def test_create_camera_preset_without_position(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset without position", - "position": {}, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_with_missing_required_keys_in_position(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset with invalid position", - "position": {"key": "value"}, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_with_position_not_number(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset with invalid position", - "position": { - "x": "not a number", - "y": 1, - "zoom": 1, - }, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_with_position_values_as_string(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset with invalid position", - "position": { - "x": "1", - "y": "1", - "zoom": "1", - }, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): - asset_bed = self.asset_bed1 - res = self.client.post( - self.get_base_url(asset_bed.external_id), - { - "name": "Preset with proper position", - "position": { - "x": 1.0, - "y": 1.0, - "zoom": 1.0, - }, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - preset_external_id = res.data["id"] - - # Check if preset in asset-bed preset list - res = self.client.get(self.get_base_url(asset_bed.external_id)) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertContains(res, preset_external_id) - - # Check if preset in asset preset list - res = self.client.get( - f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertContains(res, preset_external_id) - - # Check if preset in bed preset list - res = self.client.get( - f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertContains(res, preset_external_id) - - def test_create_camera_preset_with_same_name_in_same_bed(self): - data = { - "name": "Duplicate Preset Name", - "position": { - "x": 1.0, - "y": 1.0, - "zoom": 1.0, - }, - } - self.client.post( - self.get_base_url(self.asset_bed1.external_id), data, format="json" - ) - res = self.client.post( - self.get_base_url(self.asset_bed2.external_id), data, format="json" - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/facility/tests/test_asset_location_api.py b/care/facility/tests/test_asset_location_api.py index 9e8280d617..6bcfed7850 100644 --- a/care/facility/tests/test_asset_location_api.py +++ b/care/facility/tests/test_asset_location_api.py @@ -21,7 +21,7 @@ def setUpTestData(cls) -> None: asset_class=AssetClasses.HL7MONITOR.name, ) cls.bed = cls.create_bed(cls.facility, cls.asset_location_with_linked_bed) - cls.asset_bed = cls.create_asset_bed(cls.asset, cls.bed) + cls.asset_bed = cls.create_assetbed(cls.bed, cls.asset) cls.patient = cls.create_patient(cls.district, cls.facility) cls.consultation = cls.create_consultation(cls.patient, cls.facility) cls.consultation_bed = cls.create_consultation_bed(cls.consultation, cls.bed) @@ -36,8 +36,8 @@ def setUpTestData(cls) -> None: cls.asset_second_location, asset_class=AssetClasses.HL7MONITOR.name ) cls.asset_bed_second = cls.create_bed(cls.facility, cls.asset_second_location) - cls.assetbed_second = cls.create_asset_bed( - cls.asset_second, cls.asset_bed_second + cls.assetbed_second = cls.create_assetbed( + cls.asset_bed_second, cls.asset_second ) def test_list_asset_locations(self): diff --git a/care/facility/tests/test_assetbed_api.py b/care/facility/tests/test_assetbed_api.py new file mode 100644 index 0000000000..2129ef1a00 --- /dev/null +++ b/care/facility/tests/test_assetbed_api.py @@ -0,0 +1,531 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.facility.models import AssetBed, Bed +from care.users.models import User +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.tests.test_utils import TestUtils + + +class AssetBedViewSetTests(TestUtils, APITestCase): + """ + Test class for AssetBed + """ + + @classmethod + def setUpTestData(cls) -> None: + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.facility2 = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.user = cls.create_user( + "user", + district=cls.district, + local_body=cls.local_body, + home_facility=cls.facility, + ) # user from facility + cls.foreign_user = cls.create_user( + "foreign_user", + district=cls.district, + local_body=cls.local_body, + home_facility=cls.facility2, + ) # user outside the facility + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.asset_location1 = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location1, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.bed1 = Bed.objects.create( + name="bed1", location=cls.asset_location1, facility=cls.facility + ) + cls.asset_location2 = cls.create_asset_location(cls.facility) + # camera asset + cls.asset2 = cls.create_asset( + cls.asset_location2, asset_class=AssetClasses.ONVIF.name + ) + cls.bed2 = Bed.objects.create( + name="bed2", location=cls.asset_location2, facility=cls.facility + ) + cls.asset_location3 = cls.create_asset_location(cls.facility) + cls.asset3 = cls.create_asset( + cls.asset_location3, asset_class=AssetClasses.VENTILATOR.name + ) + cls.bed3 = Bed.objects.create( + name="bed3", location=cls.asset_location3, facility=cls.facility + ) + # for testing create, put and patch requests + cls.bed4 = Bed.objects.create( + name="bed4", location=cls.asset_location3, facility=cls.facility + ) + cls.foreign_asset_location = cls.create_asset_location(cls.facility2) + cls.foreign_asset = cls.create_asset(cls.foreign_asset_location) + cls.foreign_bed = Bed.objects.create( + name="foreign_bed", + location=cls.foreign_asset_location, + facility=cls.facility2, + ) + + cls.create_assetbed(bed=cls.bed2, asset=cls.asset2) + cls.create_assetbed(bed=cls.bed3, asset=cls.asset3) + + # assetbed for different facility + cls.create_assetbed(bed=cls.foreign_bed, asset=cls.foreign_asset) + + def setUp(self) -> None: + super().setUp() + self.assetbed = self.create_assetbed(bed=self.bed1, asset=self.asset1) + + def get_base_url(self) -> str: + return "/api/v1/assetbed" + + def get_url(self, external_id=None): + """ + Constructs the url for ambulance api + """ + base_url = f"{self.get_base_url()}/" + if external_id is not None: + base_url += f"{external_id}/" + return base_url + + def test_list_assetbed(self): + # assetbed accessible to facility 1 user (current user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 3) + + # logging in as foreign user + self.client.force_login(self.foreign_user) + + # assetbed accessible to facility 2 user (foreign user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + + # logging in as superuser + self.client.force_login(self.super_user) + + # access to all assetbed + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], AssetBed.objects.count()) + + # testing for filters + response = self.client.get(self.get_url(), {"asset": self.asset1.external_id}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], AssetBed.objects.filter(asset=self.asset1).count() + ) + + response = self.client.get(self.get_url(), {"bed": self.bed1.external_id}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], AssetBed.objects.filter(bed=self.bed1).count() + ) + self.assertEqual( + response.data["results"][0]["bed_object"]["name"], self.bed1.name + ) + + response = self.client.get( + self.get_url(), {"facility": self.facility.external_id} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], + AssetBed.objects.filter(bed__facility=self.facility).count(), + ) + + def test_create_assetbed(self): + # Missing asset and bed + missing_fields_data = {} + response = self.client.post(self.get_url(), missing_fields_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + self.assertIn("bed", response.data) + + # Invalid asset UUID + invalid_asset_uuid_data = { + "asset": "invalid-uuid", + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), invalid_asset_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + + # Invalid bed UUID + invalid_bed_uuid_data = { + "asset": str(self.asset1.external_id), + "bed": "invalid-uuid", + } + response = self.client.post( + self.get_url(), invalid_bed_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("bed", response.data) + + # Non-existent asset UUID + non_existent_asset_uuid_data = { + "asset": "11111111-1111-1111-1111-111111111111", + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), non_existent_asset_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Non-existent bed UUID + non_existent_bed_uuid_data = { + "asset": str(self.asset1.external_id), + "bed": "11111111-1111-1111-1111-111111111111", + } + response = self.client.post( + self.get_url(), non_existent_bed_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # User does not have access to foreign facility + foreign_user_data = { + "asset": str(self.foreign_asset.external_id), + "bed": str(self.foreign_bed.external_id), + } + self.client.force_login(self.user) # Ensure current user is logged in + response = self.client.post(self.get_url(), foreign_user_data, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Invalid asset class (e.g., VENTILATOR) + invalid_asset_class_data = { + "asset": str(self.asset3.external_id), # VENTILATOR asset class + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), invalid_asset_class_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + + # Asset and bed in different facilities + asset_bed_different_facilities = { + "asset": str(self.foreign_asset.external_id), + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), asset_bed_different_facilities, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Trying to create a duplicate assetbed with bed2 and asset2 (assetbed already exist with same bed and asset) + duplicate_asset_class_data = { + "asset": str(self.asset2.external_id), # asset2 is already assigned to bed2 + "bed": str(self.bed2.external_id), + } + response = self.client.post( + self.get_url(), duplicate_asset_class_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # Successfully create AssetBed with valid data + valid_data = { + "asset": str(self.asset1.external_id), + "bed": str(self.bed4.external_id), + } + response = self.client.post(self.get_url(), valid_data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_retrieve_assetbed(self): + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + def test_update_assetbed(self): + # checking old values + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["meta"], {}) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + invalid_updated_data = { + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + invalid_updated_data = { + "bed": self.bed2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + updated_data = { + "bed": self.bed4.external_id, + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assetbed.refresh_from_db() + + self.assertEqual(self.assetbed.bed.external_id, self.bed4.external_id) + self.assertEqual(self.assetbed.asset.external_id, self.asset2.external_id) + self.assertEqual(self.assetbed.meta, {"sample_data": "sample value"}) + + def test_patch_assetbed(self): + # checking old values + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["meta"], {}) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + invalid_updated_data = { + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + invalid_updated_data = { + "bed": self.bed4.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + updated_data = { + "bed": self.bed4.external_id, + "asset": self.asset2.external_id, + } + + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assetbed.refresh_from_db() + + self.assertEqual(self.assetbed.bed.external_id, self.bed4.external_id) + self.assertEqual(self.assetbed.asset.external_id, self.asset2.external_id) + + def test_delete_assetbed(self): + # confirming that the object exist + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete( + self.get_url(external_id=self.assetbed.external_id) + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # confirming if it's deleted + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # confirming using db + self.assetbed.refresh_from_db() + self.assertFalse( + AssetBed.objects.filter(external_id=self.assetbed.external_id).exists() + ) + + def test_linking_multiple_cameras_to_a_bed(self): + # We already have camera linked(asset2) to bed2 + # Attempt linking another camera to same bed. + new_camera_asset = self.create_asset( + self.asset_location2, asset_class=AssetClasses.ONVIF.name + ) + data = { + "bed": self.bed2.external_id, + "asset": new_camera_asset.external_id, + } + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_linking_multiple_hl7_monitors_to_a_bed(self): + # We already have hl7 monitor linked(asset1) to bed1) + # Attempt linking another hl7 monitor to same bed. + new_hl7_monitor_asset = self.create_asset( + self.asset_location2, asset_class=AssetClasses.HL7MONITOR.name + ) + data = { + "bed": self.bed1.external_id, + "asset": new_hl7_monitor_asset.external_id, + } + res = self.client.post("/api/v1/assetbed/", data, format="json") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + +class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.asset2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + cls.asset_bed1 = cls.create_assetbed(cls.bed, cls.asset1) + cls.asset_bed2 = cls.create_assetbed(cls.bed, cls.asset2) + + def get_base_url(self, asset_bed_id=None): + return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" + + def test_create_camera_preset_without_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset without position", + "position": {}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_missing_required_keys_in_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": {"key": "value"}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_not_number(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "not a number", + "y": 1, + "zoom": 1, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_values_as_string(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "1", + "y": "1", + "zoom": "1", + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): + asset_bed = self.asset_bed1 + res = self.client.post( + self.get_base_url(asset_bed.external_id), + { + "name": "Preset with proper position", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + preset_external_id = res.data["id"] + + # Check if preset in asset-bed preset list + res = self.client.get(self.get_base_url(asset_bed.external_id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in asset preset list + res = self.client.get( + f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in bed preset list + res = self.client.get( + f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + def test_create_camera_preset_with_same_name_in_same_bed(self): + data = { + "name": "Duplicate Preset Name", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + } + self.client.post( + self.get_base_url(self.asset_bed1.external_id), data, format="json" + ) + res = self.client.post( + self.get_base_url(self.asset_bed2.external_id), data, format="json" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/facility/tests/test_bed_api.py b/care/facility/tests/test_bed_api.py index ce334dd6e4..9bceece342 100644 --- a/care/facility/tests/test_bed_api.py +++ b/care/facility/tests/test_bed_api.py @@ -75,7 +75,7 @@ def test_list_beds(self): self.client.logout() def test_create_beds(self): - sample_data = { + base_data = { "name": "Sample Beds", "bed_type": 2, "location": self.asset_location.external_id, @@ -83,23 +83,27 @@ def test_create_beds(self): "number_of_beds": 10, "description": "This is a sample bed description", } + sample_data = base_data.copy() # Create a fresh copy of the base data response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Bed.objects.filter(bed_type=2).count(), 10) # without location + sample_data = base_data.copy() sample_data.update({"location": None}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["location"][0], "This field may not be null.") # without facility + sample_data = base_data.copy() sample_data.update({"facility": None}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["facility"][0], "This field may not be null.") # Test - if beds > 100 + sample_data = base_data.copy() sample_data.update({"number_of_beds": 200}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -108,6 +112,17 @@ def test_create_beds(self): "Cannot create more than 100 beds at once.", ) + # creating bed in different facility + sample_data = base_data.copy() + sample_data.update( + { + "location": self.asset_location2.external_id, + "facility": self.facility2.external_id, + } + ) + response = self.client.post("/api/v1/bed/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_retrieve_bed(self): response = self.client.get(f"/api/v1/bed/{self.bed1.external_id}/") self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 6facfdd3ad..37accc7c2c 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -4,7 +4,7 @@ from rest_framework import status from rest_framework.test import APITestCase -from care.facility.models import PatientNoteThreadChoices +from care.facility.models import PatientNoteThreadChoices, ShiftingRequest from care.facility.models.file_upload import FileUpload from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -913,6 +913,8 @@ def setUpTestData(cls): cls.patient.save() def test_patient_transfer(self): + # test transfer of patient to a outside facility with allow_transfer set to "True" + # test transfer patient with dob self.client.force_authenticate(user=self.super_user) response = self.client.post( f"/api/v1/patient/{self.patient.external_id}/transfer/", @@ -1000,6 +1002,166 @@ def test_transfer_disallowed_by_facility(self): "Patient transfer cannot be completed because the source facility does not permit it", ) + def test_transfer_within_facility(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "date_of_birth": "1992-04-01", + "facility": self.facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE) + self.assertEqual( + response.data["Patient"], + "Patient transfer cannot be completed because the patient has an active consultation in the same facility", + ) + + def test_transfer_without_dob(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "age": "32", + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.patient.refresh_from_db() + self.consultation.refresh_from_db() + + self.assertEqual(self.patient.facility, self.destination_facility) + + self.assertEqual( + self.consultation.new_discharge_reason, NewDischargeReasonEnum.REFERRED + ) + self.assertIsNotNone(self.consultation.discharge_date) + + def test_transfer_with_no_active_consultation(self): + # Mocking discharge of the patient + self.consultation.discharge_date = now() + self.consultation.save() + + # Ensure transfer succeeds when there's no active consultation + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh patient data + self.patient.refresh_from_db() + + # Assert the patient's facility has been updated + self.assertEqual(self.patient.facility, self.destination_facility) + + def test_transfer_with_incorrect_year_of_birth(self): + # Set the patient's facility to allow transfers + self.patient.allow_transfer = True + self.patient.save() + + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1990, # Incorrect year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data["year_of_birth"][0], "Year of birth does not match" + ) + + def test_auto_reject_active_shifting_requests_upon_transfer(self): + # Create a mock shifting request that is still active (PENDING status) + shifting_request = ShiftingRequest.objects.create( + patient=self.patient, + origin_facility=self.facility, + status=10, # PENDING status + comments="Initial request", + created_by=self.super_user, + ) + + # Perform the patient transfer + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh the shifting request and verify it was auto-rejected + shifting_request.refresh_from_db() + self.assertEqual(shifting_request.status, 30) # REJECTED status + self.assertIn( + f"The shifting request was auto rejected by the system as the patient was moved to {self.destination_facility.name}", + shifting_request.comments, + ) + + def test_transfer_with_matching_year_of_birth(self): + # Set the patient's facility to allow transfers + self.patient.allow_transfer = True + self.patient.save() + + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": self.patient.year_of_birth, # Correct year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh patient data + self.patient.refresh_from_db() + + # Assert the patient's facility has been updated + self.assertEqual(self.patient.facility, self.destination_facility) + + def test_transfer_to_non_existent_facility(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": "dff237c5-9410-4714-9101-399941b60ede", # Non-existent facility + }, + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_transfer_with_invalid_data(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": "invalid-year", # Invalid year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("year_of_birth", response.data) + + def test_unauthorized_transfer_request(self): + # Not authenticating the user to test unauthorized access + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + class PatientSearchTestCase(TestUtils, APITestCase): @classmethod diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index 334bcecfa5..cc6c59e1c4 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -1,17 +1,35 @@ import json +from typing import TypedDict +import jsonschema import requests from django.conf import settings +from jsonschema import ValidationError as JSONValidationError from rest_framework import status -from rest_framework.exceptions import APIException +from rest_framework.exceptions import APIException, ValidationError from care.utils.jwks.token_generator import generate_jwt +from .schema import meta_object_schema + + +class ActionParams(TypedDict, total=False): + type: str + data: dict | None + timeout: int | None + class BaseAssetIntegration: auth_header_type = "Care_Bearer " def __init__(self, meta): + try: + meta["_name"] = self._name + jsonschema.validate(instance=meta, schema=meta_object_schema) + except JSONValidationError as e: + error_message = f"Invalid metadata: {e.message}" + raise ValidationError(error_message) from e + self.meta = meta self.id = self.meta.get("id", "") self.host = self.meta["local_ip_address"] @@ -19,8 +37,8 @@ def __init__(self, meta): self.insecure_connection = self.meta.get("insecure_connection", False) self.timeout = settings.MIDDLEWARE_REQUEST_TIMEOUT - def handle_action(self, action): - pass + def handle_action(self, **kwargs): + """Handle actions using kwargs instead of dict.""" def get_url(self, endpoint): protocol = "http" @@ -48,16 +66,14 @@ def _validate_response(self, response: requests.Response): {"error": "Invalid Response"}, response.status_code ) from e - def api_post(self, url, data=None): + def api_post(self, url, data=None, timeout=None): + timeout = timeout or self.timeout return self._validate_response( - requests.post( - url, json=data, headers=self.get_headers(), timeout=self.timeout - ) + requests.post(url, json=data, headers=self.get_headers(), timeout=timeout) ) - def api_get(self, url, data=None): + def api_get(self, url, data=None, timeout=None): + timeout = timeout or self.timeout return self._validate_response( - requests.get( - url, params=data, headers=self.get_headers(), timeout=self.timeout - ) + requests.get(url, params=data, headers=self.get_headers(), timeout=timeout) ) diff --git a/care/utils/assetintegration/hl7monitor.py b/care/utils/assetintegration/hl7monitor.py index abd14247d3..bf331f71ca 100644 --- a/care/utils/assetintegration/hl7monitor.py +++ b/care/utils/assetintegration/hl7monitor.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class HL7MonitorAsset(BaseAssetIntegration): @@ -20,12 +20,13 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + timeout = kwargs.get("timeout") if action_type == self.HL7MonitorActions.GET_VITALS.value: request_params = {"device_id": self.host} - return self.api_get(self.get_url("vitals"), request_params) + return self.api_get(self.get_url("vitals"), request_params, timeout) if action_type == self.HL7MonitorActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -34,6 +35,7 @@ def handle_action(self, action): "asset_id": self.id, "ip": self.host, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index 815994855e..2dd814b4e6 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class OnvifAsset(BaseAssetIntegration): @@ -27,9 +27,10 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] - action_data = action.get("data", {}) + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + action_data = kwargs.get("data", {}) + timeout = kwargs.get("timeout") request_body = { "hostname": self.host, @@ -41,19 +42,19 @@ def handle_action(self, action): } if action_type == self.OnvifActions.GET_CAMERA_STATUS.value: - return self.api_get(self.get_url("status"), request_body) + return self.api_get(self.get_url("status"), request_body, timeout) if action_type == self.OnvifActions.GET_PRESETS.value: - return self.api_get(self.get_url("presets"), request_body) + return self.api_get(self.get_url("presets"), request_body, timeout) if action_type == self.OnvifActions.GOTO_PRESET.value: - return self.api_post(self.get_url("gotoPreset"), request_body) + return self.api_post(self.get_url("gotoPreset"), request_body, timeout) if action_type == self.OnvifActions.ABSOLUTE_MOVE.value: - return self.api_post(self.get_url("absoluteMove"), request_body) + return self.api_post(self.get_url("absoluteMove"), request_body, timeout) if action_type == self.OnvifActions.RELATIVE_MOVE.value: - return self.api_post(self.get_url("relativeMove"), request_body) + return self.api_post(self.get_url("relativeMove"), request_body, timeout) if action_type == self.OnvifActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -61,6 +62,7 @@ def handle_action(self, action): { "stream_id": self.access_key, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) diff --git a/care/utils/assetintegration/schema.py b/care/utils/assetintegration/schema.py new file mode 100644 index 0000000000..3396747162 --- /dev/null +++ b/care/utils/assetintegration/schema.py @@ -0,0 +1,34 @@ +meta_object_schema = { + "type": "object", + "properties": { + "id": {"type": "string"}, + "local_ip_address": {"type": "string", "format": "ipv4"}, + "middleware_hostname": {"type": "string"}, + "insecure_connection": {"type": "boolean", "default": False}, + "camera_access_key": { + "type": "string", + "pattern": "^[^:]+:[^:]+:[^:]+$", # valid pattern for "abc:def:ghi" , "123:456:789" + }, + }, + "required": ["local_ip_address", "middleware_hostname"], + "allOf": [ + { + "if": {"properties": {"_name": {"const": "onvif"}}}, + "then": { + "properties": {"camera_access_key": {"type": "string"}}, + "required": [ + "camera_access_key" + ], # Require camera_access_key for Onvif + }, + "else": { + "properties": {"id": {"type": "string"}}, + "required": ["id"], # Require id for non-Onvif assets + "not": { + "required": [ + "camera_access_key" + ] # Make camera_access_key not required for non-Onvif + }, + }, + } + ], +} diff --git a/care/utils/assetintegration/ventilator.py b/care/utils/assetintegration/ventilator.py index 23a5280960..afb896bfdb 100644 --- a/care/utils/assetintegration/ventilator.py +++ b/care/utils/assetintegration/ventilator.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class VentilatorAsset(BaseAssetIntegration): @@ -20,12 +20,13 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + timeout = kwargs.get("timeout") if action_type == self.VentilatorActions.GET_VITALS.value: request_params = {"device_id": self.host} - return self.api_get(self.get_url("vitals"), request_params) + return self.api_get(self.get_url("vitals"), request_params, timeout) if action_type == self.VentilatorActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -34,6 +35,7 @@ def handle_action(self, action): "asset_id": self.id, "ip": self.host, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 17d98574ff..da4b726469 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -4164,7 +4164,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient", + "name": "Dummy Patient One", "gender": 1, "phone_number": "+919987455444", "emergency_phone_number": "+919898797775", @@ -4247,7 +4247,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Test E2E User", + "name": "Dummy Patient Two", "gender": 1, "phone_number": "+919765259927", "emergency_phone_number": "+919228973557", @@ -4330,7 +4330,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 1", + "name": "Dummy Patient Three", "gender": 1, "phone_number": "+919192495353", "emergency_phone_number": "+919460491040", @@ -4413,7 +4413,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 2", + "name": "Dummy Patient Four", "gender": 1, "phone_number": "+919112608904", "emergency_phone_number": "+919110616234", @@ -4496,7 +4496,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 3", + "name": "Dummy Patient Five", "gender": 1, "phone_number": "+919640229897", "emergency_phone_number": "+919135436547", @@ -4579,7 +4579,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 4", + "name": "Dummy Patient Six", "gender": 1, "phone_number": "+919762277015", "emergency_phone_number": "+919342634016", @@ -4662,7 +4662,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 5", + "name": "Dummy Patient Seven", "gender": 1, "phone_number": "+919303212282", "emergency_phone_number": "+919229738916", @@ -4745,7 +4745,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 6", + "name": "Dummy Patient Eight", "gender": 1, "phone_number": "+919740701377", "emergency_phone_number": "+919321666516", @@ -4828,7 +4828,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 7", + "name": "Dummy Patient Nine", "gender": 1, "phone_number": "+919148299129", "emergency_phone_number": "+919267280161", @@ -4911,7 +4911,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 8", + "name": "Dummy Patient Ten", "gender": 1, "phone_number": "+919490490290", "emergency_phone_number": "+919828674710", @@ -4994,7 +4994,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 9", + "name": "Dummy Patient Eleven", "gender": 1, "phone_number": "+919983927490", "emergency_phone_number": "+919781111140", @@ -5077,7 +5077,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 10", + "name": "Dummy Patient Twelve", "gender": 1, "phone_number": "+919849511866", "emergency_phone_number": "+919622326248", @@ -5160,7 +5160,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 11", + "name": "Dummy Patient Thirteen", "gender": 1, "phone_number": "+919343556704", "emergency_phone_number": "+919967920474", @@ -5243,7 +5243,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 12", + "name": "Dummy Patient Fourteen", "gender": 1, "phone_number": "+919320374643", "emergency_phone_number": "+919493558024", @@ -5326,7 +5326,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 13", + "name": "Discharge Patient One", "gender": 1, "phone_number": "+919292990239", "emergency_phone_number": "+919992258784", @@ -5409,7 +5409,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 14", + "name": "Discharge Patient Two", "gender": 1, "phone_number": "+919650206292", "emergency_phone_number": "+919596454242", @@ -5492,7 +5492,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 15", + "name": "Discharge Patient Three", "gender": 1, "phone_number": "+919266236581", "emergency_phone_number": "+919835286558", @@ -5575,7 +5575,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 16", + "name": "Discharge Patient Four", "gender": 1, "phone_number": "+919243083817", "emergency_phone_number": "+919924971004", diff --git a/data/dummy/users.json b/data/dummy/users.json index e7b0115614..b86ae90ac4 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -894,14 +894,14 @@ "password": "argon2$argon2id$v=19$m=102400,t=2,p=8$bUNTR1MwejJYNXdXd2VUYjJHMmN5bw$alS6S9Ay3bvIHe9U18luyn7LyVaArgrgHIt+vh4ta48", "last_login": null, "is_superuser": false, - "first_name": "Dev", - "last_name": "Doctor Two", + "first_name": "Tester", + "last_name": "Doctor", "email": "devdoctor1@test.com", "is_staff": false, "is_active": true, "date_joined": "2024-10-14T07:53:32.400Z", "external_id": "009c4fc2-f7af-4a02-9383-6fbb4af2fdbb", - "username": "devdoctor1", + "username": "dev-doctor2", "user_type": 15, "created_by": 2, "ward": null, From 17915484d2f61c9dea75a0b6edfe48a9eddd1906 Mon Sep 17 00:00:00 2001 From: prafful Date: Tue, 26 Nov 2024 21:39:40 +0530 Subject: [PATCH 7/9] added test and url for listing available flags --- care/facility/api/viewsets/facility_flag.py | 12 +++++++++++- care/facility/tests/test_facility_flags_api.py | 11 ++++++++++- care/users/api/viewsets/user_flag.py | 12 +++++++++++- care/users/tests/test_user_flags_api.py | 11 ++++++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/care/facility/api/viewsets/facility_flag.py b/care/facility/api/viewsets/facility_flag.py index b3e18e5365..14902f2c58 100644 --- a/care/facility/api/viewsets/facility_flag.py +++ b/care/facility/api/viewsets/facility_flag.py @@ -1,5 +1,7 @@ from django_filters import rest_framework as filters -from rest_framework import viewsets +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response from care.facility.api.serializers.facility_flag import FacilityFlagSerializer from care.facility.models import FacilityFlag @@ -25,3 +27,11 @@ class FacilityFlagViewSet(viewsets.ModelViewSet): filter_backends = [filters.DjangoFilterBackend] filterset_class = FacilityFlagFilter + + @action(detail=False, methods=["get"], url_path="available-flags") + def list_available_flags(self, request): + """ + List all available flags for FacilityFlag. + """ + flags = FacilityFlag.objects.values_list("flag", flat=True).distinct() + return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) diff --git a/care/facility/tests/test_facility_flags_api.py b/care/facility/tests/test_facility_flags_api.py index 2cf0884b9b..a0973468d3 100644 --- a/care/facility/tests/test_facility_flags_api.py +++ b/care/facility/tests/test_facility_flags_api.py @@ -26,10 +26,12 @@ def setUpTestData(cls): def setUp(self): self.facility_flag_1 = self.create_facility_flag("TEST_FLAG", self.facility) - def get_url(self, facility_flag_id=None): + def get_url(self, facility_flag_id=None, action=None): base_url = "/api/v1/facility_flags/" if facility_flag_id is not None: base_url += f"{facility_flag_id}/" + if action is not None: + base_url += f"{action}/" return base_url def test_access_with_non_super_user(self): @@ -49,6 +51,13 @@ def test_list_facility_flags(self): data = response.json() self.assertEqual(data["count"], 2) + def test_list_available_flags(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url(action="available-flags")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(sorted(data["available_flags"]), ["TEST_FLAG", "TEST_FLAG_2"]) + def test_create_facility_flag(self): self.client.force_authenticate(user=self.super_user) diff --git a/care/users/api/viewsets/user_flag.py b/care/users/api/viewsets/user_flag.py index bce2ad0c3b..2d532299b2 100644 --- a/care/users/api/viewsets/user_flag.py +++ b/care/users/api/viewsets/user_flag.py @@ -1,5 +1,7 @@ from django_filters import rest_framework as filters -from rest_framework import viewsets +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response from care.users.api.serializers.user_flag import UserFlagSerializer from care.users.models import UserFlag @@ -25,3 +27,11 @@ class UserFlagViewSet(viewsets.ModelViewSet): filter_backends = [filters.DjangoFilterBackend] filterset_class = UserFlagFilter + + @action(detail=False, methods=["get"], url_path="available-flags") + def list_available_flags(self, request): + """ + List all available flags for FacilityFlag. + """ + flags = UserFlag.objects.values_list("flag", flat=True).distinct() + return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) diff --git a/care/users/tests/test_user_flags_api.py b/care/users/tests/test_user_flags_api.py index 0a2a02321c..f965f0af83 100644 --- a/care/users/tests/test_user_flags_api.py +++ b/care/users/tests/test_user_flags_api.py @@ -24,10 +24,12 @@ def setUpTestData(cls): def setUp(self): self.user_flag_1 = self.create_user_flag("TEST_FLAG", self.user) - def get_url(self, user_flag_id=None): + def get_url(self, user_flag_id=None, action=None): base_url = "/api/v1/user_flags/" if user_flag_id is not None: base_url += f"{user_flag_id}/" + if action is not None: + base_url += f"{action}/" return base_url def test_access_with_non_super_user(self): @@ -47,6 +49,13 @@ def test_list_user_flags(self): data = response.json() self.assertEqual(data["count"], 2) + def test_list_available_flags(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.get(self.get_url(action="available-flags")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(sorted(data["available_flags"]), ["TEST_FLAG", "TEST_FLAG_2"]) + def test_create_user_flag(self): self.client.force_authenticate(user=self.super_user) From 4a9b990aa0e7322a2e8b6532e338d883b7b74367 Mon Sep 17 00:00:00 2001 From: prafful Date: Wed, 27 Nov 2024 17:48:40 +0530 Subject: [PATCH 8/9] add try catch for error handling of falg endpoints --- care/facility/api/viewsets/facility_flag.py | 10 ++++++++-- care/users/api/viewsets/user_flag.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/care/facility/api/viewsets/facility_flag.py b/care/facility/api/viewsets/facility_flag.py index 14902f2c58..6f068b2131 100644 --- a/care/facility/api/viewsets/facility_flag.py +++ b/care/facility/api/viewsets/facility_flag.py @@ -33,5 +33,11 @@ def list_available_flags(self, request): """ List all available flags for FacilityFlag. """ - flags = FacilityFlag.objects.values_list("flag", flat=True).distinct() - return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) + try: + flags = FacilityFlag.objects.values_list("flag", flat=True).distinct() + return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {"error": "Failed to fetch available flags", "detail": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) diff --git a/care/users/api/viewsets/user_flag.py b/care/users/api/viewsets/user_flag.py index 2d532299b2..c75da15041 100644 --- a/care/users/api/viewsets/user_flag.py +++ b/care/users/api/viewsets/user_flag.py @@ -33,5 +33,11 @@ def list_available_flags(self, request): """ List all available flags for FacilityFlag. """ - flags = UserFlag.objects.values_list("flag", flat=True).distinct() - return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) + try: + flags = UserFlag.objects.values_list("flag", flat=True).distinct() + return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {"error": "Failed to fetch available flags", "detail": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) From f7fbea16f8a5e7923a5a45fc5c58e231ae53a4f7 Mon Sep 17 00:00:00 2001 From: Prafful Date: Tue, 3 Dec 2024 14:00:14 +0530 Subject: [PATCH 9/9] fixed changes in comments --- care/facility/api/viewsets/facility_flag.py | 7 ++++--- care/users/api/viewsets/user_flag.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/care/facility/api/viewsets/facility_flag.py b/care/facility/api/viewsets/facility_flag.py index 6f068b2131..e590b5e36f 100644 --- a/care/facility/api/viewsets/facility_flag.py +++ b/care/facility/api/viewsets/facility_flag.py @@ -6,10 +6,11 @@ from care.facility.api.serializers.facility_flag import FacilityFlagSerializer from care.facility.models import FacilityFlag from care.utils.custom_permissions import IsSuperUser +from care.utils.registries.feature_flag import FlagRegistry, FlagType class FacilityFlagFilter(filters.FilterSet): - flag = filters.CharFilter(field_name="flag", lookup_expr="icontains") + flag = filters.CharFilter(field_name="flag", lookup_expr="exact") facility = filters.UUIDFilter(field_name="facility__external_id") @@ -34,8 +35,8 @@ def list_available_flags(self, request): List all available flags for FacilityFlag. """ try: - flags = FacilityFlag.objects.values_list("flag", flat=True).distinct() - return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) + flags = FlagRegistry.get_all_flags(FlagType.FACILITY) + return Response({"available_flags": list(flags)}) except Exception as e: return Response( {"error": "Failed to fetch available flags", "detail": str(e)}, diff --git a/care/users/api/viewsets/user_flag.py b/care/users/api/viewsets/user_flag.py index c75da15041..8c6d431316 100644 --- a/care/users/api/viewsets/user_flag.py +++ b/care/users/api/viewsets/user_flag.py @@ -6,10 +6,11 @@ from care.users.api.serializers.user_flag import UserFlagSerializer from care.users.models import UserFlag from care.utils.custom_permissions import IsSuperUser +from care.utils.registries.feature_flag import FlagRegistry, FlagType class UserFlagFilter(filters.FilterSet): - flag = filters.CharFilter(field_name="flag", lookup_expr="icontains") + flag = filters.CharFilter(field_name="flag", lookup_expr="exact") user = filters.UUIDFilter(field_name="user__external_id") @@ -34,8 +35,8 @@ def list_available_flags(self, request): List all available flags for FacilityFlag. """ try: - flags = UserFlag.objects.values_list("flag", flat=True).distinct() - return Response({"available_flags": list(flags)}, status=status.HTTP_200_OK) + flags = FlagRegistry.get_all_flags(FlagType.USER) + return Response({"available_flags": list(flags)}) except Exception as e: return Response( {"error": "Failed to fetch available flags", "detail": str(e)},