Skip to content

Commit

Permalink
Merge pull request #2567 from ohcnetwork/staging
Browse files Browse the repository at this point in the history
Production Release 24.44.0
  • Loading branch information
gigincg authored Oct 28, 2024
2 parents b328f5e + 6faba33 commit f1f5662
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
58 changes: 31 additions & 27 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 9 additions & 8 deletions care/facility/api/serializers/bed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
25 changes: 24 additions & 1 deletion care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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()
Expand Down
36 changes: 36 additions & 0 deletions care/facility/tests/test_asset_bed_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
43 changes: 42 additions & 1 deletion care/facility/tests/test_asset_location_api.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -15,22 +16,62 @@ 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)
cls.user = cls.create_user("staff", cls.district, home_facility=cls.facility)
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(
f"/api/v1/facility/{self.facility.external_id}/asset_location/"
)
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(
Expand Down
14 changes: 14 additions & 0 deletions care/users/reset_password_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
45 changes: 44 additions & 1 deletion care/users/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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="[email protected]",
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):
Expand Down

0 comments on commit f1f5662

Please sign in to comment.