From fa48de84e787762355ff77fd178aa64433de2074 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:22:46 +0530 Subject: [PATCH 1/5] Bump newrelic from 10.0.0 to 10.1.0 (#2541) Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 10.0.0 to 10.1.0. - [Release notes](https://github.com/newrelic/newrelic-python-agent/releases) - [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v10.0.0...v10.1.0) --- updated-dependencies: - dependency-name: newrelic dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- Pipfile | 2 +- Pipfile.lock | 58 ++++++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/Pipfile b/Pipfile index 16f5923139..c95fa9289a 100644 --- a/Pipfile +++ b/Pipfile @@ -29,7 +29,7 @@ gunicorn = "==23.0.0" healthy-django = "==0.1.0" jsonschema = "==4.23.0" jwcrypto = "==1.5.6" -newrelic = "==10.0.0" +newrelic = "==10.1.0" pillow = "==10.4.0" psycopg = { extras = ["c"], version = "==3.2.2" } pycryptodome = "==3.20.0" diff --git a/Pipfile.lock b/Pipfile.lock index dda896d1fc..ee060a0463 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "82b40d895920ac109f51b1e331c891ba2c9e3ebe5e7feded2cd8cc01bbd948d0" + "sha256": "1c90ca755b86427eedcf4f48ff4aa792fd98b9ad9e0462f8463ba8e7283f9949" }, "pipfile-spec": 6, "requires": { @@ -1005,35 +1005,39 @@ }, "newrelic": { "hashes": [ - "sha256:002e21527c77c0c9640402c152d40a114b4cc821e7de93cf445fffaef160f1aa", - "sha256:01e68cf6826a3d456aaa0a4c88a7b864403428369b855c3d9c5c27958ef48adb", - "sha256:03a5068d68f22d80797a048a4018d673b8cdd646bc5f9fb63328b53b08bc6de7", - "sha256:0d1f0c1c54a301ee8f7c4372a8905a18cd36d9a2f9b6550898dd7bac147480d3", - "sha256:14e675e0a73e52fde94df9de89201de945cc3a2a046b4fdfe5ba1717b15cad78", - "sha256:1f4cd5ca11f08badd4b1cdd746053cfb30a09d5d9b9c1f5d911718d2870b4493", - "sha256:27d2f34bf714ef9d7ff8a68265a2094b87a4bdc7b1bbbd0a1421cf5cf8f33311", - "sha256:34b60d16d6e8fbc3e65a7d5171718999ecf7bc369cf8baae1bee3c6317972e18", - "sha256:4d09af04f86d40c534d3753bdc1e45e9ca76ce85cea7ea87994c77b3d0677381", - "sha256:548b538c3e95b589a30565bff668285ca74bb64069eb1d6f643bde9768944f53", - "sha256:6257413c9e261e8256be5cadb488945dbb3830dcc6091805fa3a5c70992a03a6", - "sha256:78bc57206c7747f7096ed081d828719def7c0952ea7834c7769d383bd7ba0aa6", - "sha256:8716867245ebe97656017e7a6ef17ebccb730e59062531e3e7b9ce9ffc7b4e4b", - "sha256:94c94a0a05e2995dff812f4fb85113227bcc5a24635539031842af9c1ddc4368", - "sha256:a8a16dfac53914dd0b930a2c087df701585d4f372b2c138466418e78d067b50f", - "sha256:a8c1b480bbe5c3e2e156f8de86182aa207430dbb32e0e5dc523ba8c3731328fc", - "sha256:b1352b6d357e82899ff102acb6971fb9c2cebe70c783081f11c3e53fddfedc4e", - "sha256:b60407fdb9798eee54488130bf87dfe542c3f04475c0c6b8c14e84274db5b1eb", - "sha256:bd3c73bbfac0a48402583aada21bf026161df8b73c6552cb8654f4a93f409860", - "sha256:c633972d88d89a2a17471b834961a889709f4970016e9641e3ddc0234669aadf", - "sha256:d6e09a66088431356c6c1a75bd1cdac2e425d547b47026138b254ac51d5df23d", - "sha256:d8bfbbb50ccc39a51a3449cdfb61970d7e61be0eb93336e3725857b7c1d17ff7", - "sha256:d90d41d78bd72d7fab7ed1cf34bf3dc519ab2d8c6820554061b8708eb7951374", - "sha256:f1aac4a5fe1d0cbe2bb9e2c52152604fb872a6bce28e129febd29d1d307df1f4", - "sha256:f446bf0943220e114e861bbc96761733e0877684ee860cf61755abe2d9805367" + "sha256:09cd9a92c0cc54ec10d0954518ee490168215c13d8b24dc2398d7f539a422442", + "sha256:0acfc2fbc41c25902ad90b30a623c7cd6663c720ff754b051d39e85d179adc7a", + "sha256:24ab3764904c093fe0b3ba2d5e951c887311180b0a039ccb3cfcc6e720abdd04", + "sha256:2f0691c056c2d0df0f268dc1d39ed9fd1bdac36250316d0e4fe4547adad9de20", + "sha256:315b6675a28611409536a9fcc8cc2a463e0c662fa5f5a385267d3e672da1ae88", + "sha256:35f88e14f6448d7fb45c9a2e9685e8a51b42c859b9075df7934c84a1244db738", + "sha256:379a4e815cd48106c4b9080be7d331088fefe8ae2568eb68975fa7f617619d52", + "sha256:38f4dc5800173636eedd607a6a4c1a7a228953ac86c92ac80a6191fb050ed2c0", + "sha256:55633a0655327cb769b2d0672861cea42cd535d0e5ef73bba0cbfd3a91603a0a", + "sha256:5fefb4de5fda9273e40d6242c88d7b3775fe096940e140123e0da21829b0720c", + "sha256:7c0317ebe7f2cd61fe7131e43c5dc7965ed4bfc5f64ee631fb304e9c86b8e1e0", + "sha256:80327c8c06f2e7fa7a58efc2dd7e00d480310f81055cc9970cd356d2de1ad655", + "sha256:99968ce04e5eae99a4ab7b58baf85556dbd665f947a8f816cc6e3bacad25785a", + "sha256:a9b3ebfbd63ade4d340feea77824f62f15a8d7d1ecba55c6437e9b32106a33d2", + "sha256:ad3e5ef7181fc1d35b59041f280e55a3195d6adc0b9c0e708e30b948c2ee0848", + "sha256:afcdcbc2be3c8f7926ddcd4559186f9fb4c37f0eee9c5a480b84870381e63f34", + "sha256:affdb3ee375d47bf1995ee4d17c8a817232ed2d88bb80438e6c5f13912b18b60", + "sha256:b1db3ec4f72a4691e9fd828f1665d0f823a077b479d0a6e690b8e9c31a34bc17", + "sha256:b77f6fbfb57b0b1fdcb5cc970a4fcac60b33c1500d4611061d8009515bf07783", + "sha256:ba231e7d45b6b12a5df233a2d33677fbf9fc77f45d5332a75e11bf190fac3152", + "sha256:c2c516a1a44c2c6ba9f2bc8e1a76f7b499c1fc62307f72733b1121a3de15a11b", + "sha256:d0f0fff322dc66658fe45d945268918dc89cc33d685246ea2062aac6c9cff821", + "sha256:d4845761aca14048118d7914ca765374a967b0665f84c2603609d259c93c82ba", + "sha256:d51db5922fc549b69e7b4fae70e9a91011a079e99bb380e8cc06933e6c4d26f7", + "sha256:dd139bb4c36658007ddb17b53b8e46c6a81e6e814806b79275b9cc0d3bd626a8", + "sha256:e44f0b8e938197d13e2ef23028882018e2ecbb1c51a974b34d5329482a4584f7", + "sha256:e7a0f80519aff6f52276075b4b6222eff6dbc6814073fc017034f4869c12c93b", + "sha256:f5920bf78a844e7c5bb267b4a44978e22f125618aafa32459211ab9208da3ab5", + "sha256:f6a082558163f5e36f6ef62081aaeb3528c3dbe2489bce7ecccdbea28363c02a" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==10.0.0" + "version": "==10.1.0" }, "packaging": { "hashes": [ From 8bac44a73e272fafb736ec59ca6cda330ffe1c04 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:26:04 +0530 Subject: [PATCH 2/5] added check for email provider check before sending reset password email (#2544) * added check for email provider check before sending email * improving test to check if the email is sent successfully --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/users/reset_password_views.py | 14 ++++++++++ care/users/tests/test_auth.py | 45 +++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/care/users/reset_password_views.py b/care/users/reset_password_views.py index 89f67ae087..e204ab0719 100644 --- a/care/users/reset_password_views.py +++ b/care/users/reset_password_views.py @@ -208,6 +208,20 @@ def post(self, request, *args, **kwargs): status=status.HTTP_429_TOO_MANY_REQUESTS, ) + if settings.IS_PRODUCTION and ( + not settings.EMAIL_HOST + or not settings.EMAIL_HOST_USER + or not settings.EMAIL_HOST_PASSWORD + ): + raise exceptions.ValidationError( + { + "detail": [ + _( + "There was a problem resetting your password. Please contact the administrator." + ) + ] + } + ) # before we continue, delete all existing expired tokens password_reset_token_validation_time = get_password_reset_token_expiry_time() diff --git a/care/users/tests/test_auth.py b/care/users/tests/test_auth.py index 695e105564..912e5da010 100644 --- a/care/users/tests/test_auth.py +++ b/care/users/tests/test_auth.py @@ -1,5 +1,6 @@ from datetime import timedelta +from django.core import mail from django.test import override_settings from django.utils.timezone import now from django_rest_passwordreset.models import ResetPasswordToken @@ -99,7 +100,7 @@ def test_auth_verify_with_invalid_token(self): self.assertEqual(response.data["detail"], "Token is invalid or expired") -@override_settings(DISABLE_RATELIMIT=True) +@override_settings(DISABLE_RATELIMIT=True, IS_PRODUCTION=False) class TestPasswordReset(TestUtils, APITestCase): @classmethod def setUpTestData(cls) -> None: @@ -118,13 +119,55 @@ def create_reset_password_token( token.save() return token + @override_settings( + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + ) def test_forgot_password_with_valid_input(self): + mail.outbox = [] + response = self.client.post( + "/api/v1/password_reset/", + {"username": self.user.username}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("Password Reset for Care", mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [self.user.email]) + self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) + self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) + + @override_settings(IS_PRODUCTION=True) + def test_forgot_password_without_email_configration(self): + response = self.client.post( + "/api/v1/password_reset/", + {"username": self.user.username}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.json()["detail"][0], + "There was a problem resetting your password. Please contact the administrator.", + ) + + @override_settings( + IS_PRODUCTION=True, + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + EMAIL_HOST="dummy.smtp.server", + EMAIL_HOST_USER="dummy-email@example.com", + EMAIL_HOST_PASSWORD="dummy-password", + ) + def test_forgot_password_with_email_configuration(self): + mail.outbox = [] + response = self.client.post( "/api/v1/password_reset/", {"username": self.user.username}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("Password Reset for Care", mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [self.user.email]) self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) def test_forgot_password_with_missing_fields(self): From 3bd5e4e048dd3b7cc7fba53d9b903dfca345ef54 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 23 Oct 2024 20:02:21 +0530 Subject: [PATCH 3/5] fixes validation preventing linking multiple cameras to a bed (#2559) --- care/facility/api/serializers/bed.py | 17 ++++++----- care/facility/tests/test_asset_bed_api.py | 36 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 508c2f9619..031d2a68c1 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -111,6 +111,10 @@ def validate(self, attrs): not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): raise PermissionError + if AssetBed.objects.filter(asset=asset, bed=bed).exists(): + raise ValidationError( + {"non_field_errors": "Asset is already linked to bed"} + ) if asset.asset_class not in [ AssetClasses.HL7MONITOR.name, AssetClasses.ONVIF.name, @@ -123,18 +127,15 @@ def validate(self, attrs): {"asset": "Should be in the same facility as the bed"} ) if ( - asset.asset_class - in [ - AssetClasses.HL7MONITOR.name, - AssetClasses.ONVIF.name, - ] + asset.asset_class == AssetClasses.HL7MONITOR.name + and AssetBed.objects.filter( + bed=bed, asset__asset_class=asset.asset_class + ).exists() ) and AssetBed.objects.filter( bed=bed, asset__asset_class=asset.asset_class ).exists(): raise ValidationError( - { - "asset": "Bed is already in use by another asset of the same class" - } + {"asset": "Another HL7 Monitor is already linked to this bed."} ) else: raise ValidationError( diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py index d22aae9bfd..4ed81a36b8 100644 --- a/care/facility/tests/test_asset_bed_api.py +++ b/care/facility/tests/test_asset_bed_api.py @@ -21,9 +21,21 @@ def setUpTestData(cls): ) 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): @@ -49,6 +61,30 @@ def test_link_asset_to_bed_and_attempt_duplicate_linking(self): 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 From 5b735dcc08386daba5de0afbb8dddf87561b4858 Mon Sep 17 00:00:00 2001 From: Vysakh Premkumar <84713473+tellmeY18@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:13:00 +0530 Subject: [PATCH 4/5] Update deploy.yml (#2562) --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cccc03b16c..34b73cff0a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -102,7 +102,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} build-args: | APP_VERSION=${{ github.sha }} - ADDITIONAL_PLUGS=${{ secrets.ADDITIONAL_PLUGS }} + ADDITIONAL_PLUGS=${{ env.ADDITIONAL_PLUGS }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max From 69c2b831457ba1e47b68ada0eae9582d3c32b865 Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan <40040905+Jacobjeevan@users.noreply.github.com> Date: Thu, 24 Oct 2024 21:27:23 +0530 Subject: [PATCH 5/5] Asset location route: modifications for CNS (#2545) * CNS location - Add a parameter to filter for locations with no monitors - Also filter for monitors without paitents * Added new test cases for asset locations * using asset class instead of string * fix formatting --- care/facility/api/viewsets/asset.py | 25 ++++++++++- .../facility/tests/test_asset_location_api.py | 43 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index fc66eff4bf..8b24bebb51 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -58,6 +58,7 @@ AvailabilityRecord, StatusChoices, ) +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.cache.cache_allowed_facilities import get_accessible_facilities @@ -84,6 +85,27 @@ def delete_asset_cache(sender, instance, created, **kwargs): cache.delete("asset:qr:" + str(instance.id)) +class AssetLocationFilter(filters.FilterSet): + bed_is_occupied = filters.BooleanFilter(method="filter_bed_is_occupied") + + def filter_bed_is_occupied(self, queryset, name, value): + asset_locations = ( + AssetBed.objects.select_related("asset", "bed") + .filter(asset__asset_class=AssetClasses.HL7MONITOR.name) + .values_list("bed__location_id", "bed__id") + ) + if value: + asset_locations = asset_locations.filter( + bed__id__in=Subquery( + ConsultationBed.objects.filter( + bed__id=OuterRef("bed__id"), end_date__isnull=value + ).values("bed__id") + ) + ) + asset_locations = asset_locations.values_list("bed__location_id", flat=True) + return queryset.filter(id__in=asset_locations) + + class AssetLocationViewSet( ListModelMixin, RetrieveModelMixin, @@ -101,8 +123,9 @@ class AssetLocationViewSet( ) serializer_class = AssetLocationSerializer lookup_field = "external_id" - filter_backends = (drf_filters.SearchFilter,) + filter_backends = (filters.DjangoFilterBackend, drf_filters.SearchFilter) search_fields = ["name"] + filterset_class = AssetLocationFilter def get_serializer_context(self): facility = self.get_facility() diff --git a/care/facility/tests/test_asset_location_api.py b/care/facility/tests/test_asset_location_api.py index c2f95b8940..9e8280d617 100644 --- a/care/facility/tests/test_asset_location_api.py +++ b/care/facility/tests/test_asset_location_api.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase +from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.tests.test_utils import TestUtils @@ -15,8 +16,12 @@ def setUpTestData(cls) -> None: cls.asset_location = cls.create_asset_location(cls.facility) cls.asset_location_with_linked_bed = cls.create_asset_location(cls.facility) cls.asset_location_with_linked_asset = cls.create_asset_location(cls.facility) - cls.asset = cls.create_asset(cls.asset_location_with_linked_asset) + cls.asset = cls.create_asset( + cls.asset_location_with_linked_asset, + 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.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) @@ -24,6 +29,16 @@ def setUpTestData(cls) -> None: cls.deleted_asset = cls.create_asset(cls.asset_location) cls.deleted_asset.deleted = True cls.deleted_asset.save() + cls.asset_second_location = cls.create_asset_location( + cls.facility, name="asset2 location" + ) + cls.asset_second = cls.create_asset( + 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 + ) def test_list_asset_locations(self): response = self.client.get( @@ -31,6 +46,32 @@ def test_list_asset_locations(self): ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertContains(response, self.asset_location.external_id) + self.assertContains(response, self.asset_second_location.external_id) + + def test_asset_locations_get_monitors_all(self): + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=false" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_location_with_linked_bed.external_id) + self.assertContains(response, self.asset_second_location.external_id) + + def test_asset_locations_get_monitors_only_consultation_bed(self): + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=true" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_location_with_linked_bed.external_id) + + def test_asset_locations_get_only_monitors(self): + self.asset.asset_class = AssetClasses.VENTILATOR.name + self.asset.save() + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=false" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_second_location.external_id) + self.assertEqual(len(response.data["results"]), 1) def test_retrieve_asset_location(self): response = self.client.get(