From 4106fe67e6e5b192b4505fbdd5429900291d4de3 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 27 Sep 2023 01:26:47 +0530 Subject: [PATCH] Added `AssetPublicQRViewSet` to handle QR code based asset retrieval (#1625) * Add public asset endpoint by qr_code_id * Add tests * use get_object_or_404 * add signals and tests * manual cache test --------- Co-authored-by: Aakash Singh --- care/facility/api/viewsets/asset.py | 41 ++++++++++++++++ care/facility/tests/test_asset_public_api.py | 49 +++++++++++++++++++- care/utils/tests/test_utils.py | 3 +- config/api_router.py | 2 + 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index dbccf1ac0b..d1b7446bc4 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -1,6 +1,10 @@ +import uuid + from django.conf import settings from django.core.cache import cache from django.db.models import Exists, OuterRef, Q +from django.db.models.signals import post_save +from django.dispatch import receiver from django.http import Http404 from django.shortcuts import get_object_or_404 from django.utils import timezone @@ -58,6 +62,13 @@ inverse_asset_status = inverse_choices(StatusChoices) +@receiver(post_save, sender=Asset) +def delete_asset_cache(sender, instance, created, **kwargs): + cache.delete("asset:" + str(instance.external_id)) + cache.delete("asset:qr:" + str(instance.qr_code_id)) + cache.delete("asset:qr:" + str(instance.id)) + + class AssetLocationViewSet( ListModelMixin, RetrieveModelMixin, @@ -175,6 +186,36 @@ def retrieve(self, request, *args, **kwargs): return Response(hit) +class AssetPublicQRViewSet(GenericViewSet): + queryset = Asset.objects.all() + serializer_class = AssetSerializer + lookup_field = "qr_code_id" + + def retrieve(self, request, *args, **kwargs): + is_uuid = True + try: + uuid.UUID(kwargs["qr_code_id"]) + except ValueError: + # If the qr_code_id is not a UUID, then it is the pk of the asset + is_uuid = False + if not kwargs["qr_code_id"].isnumeric(): + return Response(status=status.HTTP_404_NOT_FOUND) + + key = "asset:qr:" + kwargs["qr_code_id"] + hit = cache.get(key) + if not hit: + if is_uuid: + instance = self.get_object() + else: + instance = get_object_or_404( + self.get_queryset(), pk=kwargs["qr_code_id"] + ) + serializer = self.get_serializer(instance) + cache.set(key, serializer.data, 60 * 60 * 24) + return Response(serializer.data) + return Response(hit) + + class AssetAvailabilityFilter(filters.FilterSet): external_id = filters.CharFilter(field_name="asset__external_id") diff --git a/care/facility/tests/test_asset_public_api.py b/care/facility/tests/test_asset_public_api.py index 7ad9f579c3..12b728c45e 100644 --- a/care/facility/tests/test_asset_public_api.py +++ b/care/facility/tests/test_asset_public_api.py @@ -1,7 +1,9 @@ +from django.core.cache import cache from rest_framework import status from rest_framework.test import APITestCase -from care.utils.tests.test_utils import TestUtils +from care.facility.api.serializers.asset import AssetSerializer +from care.utils.tests.test_utils import TestUtils, override_cache class AssetPublicViewSetTestCase(TestUtils, APITestCase): @@ -23,3 +25,48 @@ def test_retrieve_asset(self): def test_retrieve_nonexistent_asset(self): response = self.client.get("/api/v1/public/asset/nonexistent/") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_retrieve_asset_qr_code(self): + response = self.client.get(f"/api/v1/public/asset_qr/{self.asset.qr_code_id}/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.get(f"/api/v1/public/asset_qr/{self.asset.id}/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_retrieve_nonexistent_asset_qr_code(self): + response = self.client.get("/api/v1/public/asset_qr/nonexistent/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_retrieve_asset_qr_cached(self): + with override_cache(self): + response = self.client.get( + f"/api/v1/public/asset_qr/{self.asset.qr_code_id}/" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["name"], self.asset.name) + + # Update the asset to invalidate the cache + + updated_data = { + "name": "New Updated Test Asset", + } + response = self.client.patch( + f"/api/v1/asset/{self.asset.external_id}/", updated_data + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.get( + f"/api/v1/public/asset_qr/{self.asset.qr_code_id}/" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["name"], updated_data["name"]) + + def test_retrieve_asset_qr_pre_cached(self): + with override_cache(self): + serializer = AssetSerializer(self.asset) + cache.set(f"asset:qr:{self.asset.qr_code_id}", serializer.data) + response = self.client.get( + f"/api/v1/public/asset_qr/{self.asset.qr_code_id}/" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["name"], self.asset.name) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index d5cb0de873..0c884afba2 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -340,7 +340,8 @@ def create_asset(cls, location: AssetLocation, **kwargs) -> Asset: "name": "Test Asset", "current_location": location, "asset_type": 50, - "warranty_amc_end_of_validity": make_aware(datetime(2030, 4, 1)), + "warranty_amc_end_of_validity": make_aware(datetime(2030, 4, 1)).date(), + "qr_code_id": "3dcee5fa-8fb8-4b07-be12-8e0d0baf6692", } data.update(kwargs) return Asset.objects.create(**data) diff --git a/config/api_router.py b/config/api_router.py index 33b612d8d1..289df33b2a 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -13,6 +13,7 @@ from care.facility.api.viewsets.asset import ( AssetAvailabilityViewSet, AssetLocationViewSet, + AssetPublicQRViewSet, AssetPublicViewSet, AssetServiceViewSet, AssetTransactionViewSet, @@ -219,6 +220,7 @@ # Public endpoints router.register("public/asset", AssetPublicViewSet) +router.register("public/asset_qr", AssetPublicQRViewSet) # ABDM endpoints if settings.ENABLE_ABDM: