From f87473da8486e5ec70db30b1f226d4c14b53d593 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 18 Aug 2023 19:03:07 +0530 Subject: [PATCH] add consultation specific assets (#1524) --- care/facility/api/serializers/bed.py | 122 +++++++++++++----- care/facility/api/viewsets/asset.py | 33 +++++ care/facility/api/viewsets/bed.py | 1 + .../api/viewsets/patient_consultation.py | 4 + ...ultationbedasset_consultationbed_assets.py | 67 ++++++++++ care/facility/models/bed.py | 14 ++ .../tests/test_consultation_bed_asset_api.py | 51 ++++++++ 7 files changed, 262 insertions(+), 30 deletions(-) create mode 100644 care/facility/migrations/0378_consultationbedasset_consultationbed_assets.py create mode 100644 care/facility/tests/test_consultation_bed_asset_api.py diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index a44180b0c1..8eaa245fc4 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -1,8 +1,11 @@ +from django.db import transaction +from django.db.models import Q from django.shortcuts import get_object_or_404 from rest_framework.exceptions import ValidationError from rest_framework.serializers import ( BooleanField, IntegerField, + ListField, ModelSerializer, SerializerMethodField, UUIDField, @@ -11,11 +14,17 @@ from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.api.serializers.asset import AssetLocationSerializer, AssetSerializer from care.facility.models.asset import Asset, AssetLocation -from care.facility.models.bed import AssetBed, Bed, ConsultationBed +from care.facility.models.bed import ( + AssetBed, + Bed, + ConsultationBed, + ConsultationBedAsset, +) from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration from care.facility.models.patient_base import BedTypeChoices from care.facility.models.patient_consultation import PatientConsultation +from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.queryset.consultation import get_consultation_queryset from care.utils.queryset.facility import get_facility_queryset from care.utils.serializer.external_id_field import ExternalIdSerializerField @@ -85,13 +94,17 @@ class Meta: def validate(self, attrs): user = self.context["request"].user if "asset" in attrs and "bed" in attrs: - asset = get_object_or_404(Asset.objects.filter(external_id=attrs["asset"])) - bed = get_object_or_404(Bed.objects.filter(external_id=attrs["bed"])) + asset: Asset = get_object_or_404( + Asset.objects.filter(external_id=attrs["asset"]) + ) + bed: Bed = get_object_or_404(Bed.objects.filter(external_id=attrs["bed"])) facilities = get_facility_queryset(user) if ( not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): raise PermissionError() + if asset.asset_class not in [AssetClasses.HL7MONITOR, AssetClasses.ONVIF]: + raise ValidationError({"asset": "Asset is not a monitor or camera"}) attrs["asset"] = asset attrs["bed"] = bed if asset.current_location.facility.id != bed.facility.id: @@ -147,6 +160,9 @@ class ConsultationBedSerializer(ModelSerializer): queryset=Bed.objects.all(), write_only=True, required=True ) + assets = ListField(child=UUIDField(), required=False, write_only=True) + assets_objects = AssetSerializer(source="assets", many=True, read_only=True) + class Meta: model = ConsultationBed exclude = ("deleted", "external_id") @@ -157,27 +173,45 @@ def validate(self, attrs): if "consultation" in attrs and "bed" in attrs and "start_date" in attrs: bed = attrs["bed"] facilities = get_facility_queryset(user) + if not facilities.filter(id=bed.facility_id).exists(): + raise PermissionError() + permitted_consultations = get_consultation_queryset(user) - consultation = get_object_or_404( + consultation: PatientConsultation = get_object_or_404( permitted_consultations.filter(id=attrs["consultation"].id) ) - if not facilities.filter(id=bed.facility.id).exists(): - raise PermissionError() - if consultation.facility.id != bed.facility.id: + if not consultation.patient.is_active: + raise ValidationError( + {"patient:": ["Patient is already discharged from CARE"]} + ) + + if consultation.facility_id != bed.facility_id: raise ValidationError( {"consultation": "Should be in the same facility as the bed"} ) + + previous_consultation_bed = consultation.current_bed + if ( + previous_consultation_bed + and previous_consultation_bed.bed == bed + and set( + previous_consultation_bed.assets.order_by( + "external_id" + ).values_list("external_id", flat=True) + ) + == set(attrs.get("assets", [])) + ): + raise ValidationError( + {"consultation": "These set of bed and assets are already assigned"} + ) + start_date = attrs["start_date"] end_date = attrs.get("end_date", None) - existing_qs = ConsultationBed.objects.filter( - consultation=consultation, bed=bed - ) + qs = ConsultationBed.objects.filter(consultation=consultation) # Validations based of the latest entry if qs.exists(): latest_qs = qs.latest("id") - if latest_qs.bed == bed: - raise ValidationError({"bed": "Bed is already in use"}) if start_date < latest_qs.start_date: raise ValidationError( { @@ -188,8 +222,9 @@ def validate(self, attrs): raise ValidationError( {"end_date": "End date cannot be before the latest start date"} ) - existing_qs = ConsultationBed.objects.filter(consultation=consultation) + # Conflict checking logic + existing_qs = ConsultationBed.objects.filter(consultation=consultation) if existing_qs.filter(start_date__gt=start_date).exists(): raise ValidationError({"start_date": "Cannot create conflicting entry"}) if end_date: @@ -199,6 +234,7 @@ def validate(self, attrs): raise ValidationError( {"end_date": "Cannot create conflicting entry"} ) + else: raise ValidationError( { @@ -211,24 +247,50 @@ def validate(self, attrs): def create(self, validated_data): consultation = validated_data["consultation"] - bed = validated_data["bed"] - - if not consultation.patient.is_active: - raise ValidationError( - {"patient:": ["Patient is already discharged from CARE"]} - ) - occupied_beds = ConsultationBed.objects.filter(end_date__isnull=True) + with transaction.atomic(): + ConsultationBed.objects.filter( + end_date__isnull=True, consultation=consultation + ).update(end_date=validated_data["start_date"]) + if assets_ids := validated_data.pop("assets", None): + assets = ( + Asset.objects.filter( + Q(assigned_consultation_beds__isnull=True) + | Q(assigned_consultation_beds__end_date__isnull=False), + external_id__in=assets_ids, + current_location__facility=consultation.facility_id, + ) + .exclude( + asset_class__in=[ + AssetClasses.HL7MONITOR, + AssetClasses.ONVIF, + ] + ) + .values_list("external_id", flat=True) + ) + not_found_assets = list(set(assets_ids) - set(assets)) + if not_found_assets: + raise ValidationError( + f"Some assets are not available - {' ,'.join(not_found_assets)}" + ) + obj: ConsultationBed = super().create(validated_data) + if assets_ids: + asset_objects = Asset.objects.filter(external_id__in=assets_ids).only( + "id" + ) + ConsultationBedAsset.objects.bulk_create( + [ + ConsultationBedAsset(consultation_bed=obj, asset=asset) + for asset in asset_objects + ] + ) - if occupied_beds.filter(bed=bed).exists(): - raise ValidationError({"bed:": ["Bed already in use by patient"]}) + consultation.current_bed = obj + consultation.save(update_fields=["current_bed"]) + return obj - occupied_beds.filter(consultation=consultation).update( - end_date=validated_data["start_date"] - ) + def update(self, instance: ConsultationBed, validated_data) -> ConsultationBed: + # assets once linked are not allowed to be changed + validated_data.pop("assets", None) - # This needs better logic, when an update occurs and the latest bed is no longer the last bed consultation relation added. - obj = super().create(validated_data) - consultation.current_bed = obj - consultation.save(update_fields=["current_bed"]) - return obj + return super().update(instance, validated_data) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 2b5fc6e6b1..7d999a8cb8 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -110,6 +110,39 @@ class AssetFilter(filters.FilterSet): status = CareChoiceFilter(choice_dict=inverse_asset_status) is_working = filters.BooleanFilter() qr_code_id = filters.CharFilter(field_name="qr_code_id", lookup_expr="icontains") + in_use_by_consultation = filters.BooleanFilter( + method="filter_in_use_by_consultation" + ) + is_permanent = filters.BooleanFilter(method="filter_is_permanent") + + def filter_in_use_by_consultation(self, queryset, name, value): + if value is None: + return queryset + if value: + return queryset.filter(assigned_consultation_beds__end_date__isnull=True) + else: + return queryset.filter( + Q(assigned_consultation_beds__isnull=True) + | Q(assigned_consultation_beds__end_date__isnull=False) + ) + + def filter_is_permanent(self, queryset, name, value): + if value is None: + return queryset + if value: + return queryset.filter( + asset_class__in=[ + AssetClasses.ONVIF.name, + AssetClasses.HL7MONITOR.name, + ] + ) + else: + return queryset.exclude( + asset_class__in=[ + AssetClasses.ONVIF.name, + AssetClasses.HL7MONITOR.name, + ] + ) class AssetPublicViewSet(GenericViewSet): diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index d1b2cf78e1..263f6e7627 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -211,6 +211,7 @@ class ConsultationBedViewSet( queryset = ( ConsultationBed.objects.all() .select_related("consultation", "bed") + .prefetch_related("assets") .order_by("-created_date") ) serializer_class = ConsultationBedSerializer diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index cd9c3fa861..b87856d594 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -78,6 +78,10 @@ def get_queryset(self): "assigned_to__skills", queryset=Skill.objects.filter(userskill__deleted=False), ), + "current_bed", + "current_bed__bed", + "current_bed__assets", + "current_bed__assets__current_location", ) if self.request.user.is_superuser: return self.queryset diff --git a/care/facility/migrations/0378_consultationbedasset_consultationbed_assets.py b/care/facility/migrations/0378_consultationbedasset_consultationbed_assets.py new file mode 100644 index 0000000000..f2e95446cf --- /dev/null +++ b/care/facility/migrations/0378_consultationbedasset_consultationbed_assets.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.2 on 2023-08-16 12:44 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0377_merge_20230809_0009"), + ] + + operations = [ + migrations.CreateModel( + name="ConsultationBedAsset", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ( + "asset", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="facility.asset" + ), + ), + ( + "consultation_bed", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="facility.consultationbed", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="consultationbed", + name="assets", + field=models.ManyToManyField( + related_name="assigned_consultation_beds", + through="facility.ConsultationBedAsset", + to="facility.asset", + ), + ), + ] diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index 3f055db8a8..205127711f 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -73,3 +73,17 @@ class ConsultationBed(BaseModel): start_date = models.DateTimeField(null=False, blank=False) end_date = models.DateTimeField(null=True, blank=True, default=None) meta = JSONField(default=dict, blank=True) + assets = models.ManyToManyField( + Asset, through="ConsultationBedAsset", related_name="assigned_consultation_beds" + ) + + +class ConsultationBedAsset(BaseModel): + consultation_bed = models.ForeignKey( + ConsultationBed, + on_delete=models.PROTECT, + ) + asset = models.ForeignKey( + Asset, + on_delete=models.PROTECT, + ) diff --git a/care/facility/tests/test_consultation_bed_asset_api.py b/care/facility/tests/test_consultation_bed_asset_api.py new file mode 100644 index 0000000000..7b9925e4f4 --- /dev/null +++ b/care/facility/tests/test_consultation_bed_asset_api.py @@ -0,0 +1,51 @@ +from datetime import datetime + +from django.db.models import Q +from rest_framework import status + +from care.facility.models import Asset, AssetLocation, Bed, ConsultationBedAsset +from care.utils.tests.test_base import TestBase + + +class ConsultationBedAssetApiTestCase(TestBase): + def create_asset(self, **kwargs): + return Asset.objects.create(**kwargs) + + def setUp(self) -> None: + super().setUp() + self.asset_location: AssetLocation = AssetLocation.objects.create( + name="asset location", location_type=1, facility=self.facility + ) + self.bed1 = Bed.objects.create( + name="bed1", location=self.asset_location, facility=self.facility + ) + self.asset1 = self.create_asset( + name="asset1", current_location=self.asset_location + ) + self.asset2 = self.create_asset( + name="asset2", current_location=self.asset_location + ) + self.asset3 = self.create_asset( + name="asset3", current_location=self.asset_location + ) + + def test_link_asset_to_consultation_bed(self): + consultation = self.create_consultation() + response = self.client.post( + "/api/v1/consultationbed/", + { + "consultation": consultation.external_id, + "bed": self.bed1.external_id, + "start_date": datetime.now().isoformat(), + "assets": [self.asset1.external_id, self.asset2.external_id], + }, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(ConsultationBedAsset.objects.count(), 2) + self.assertEqual( + Asset.objects.filter( + Q(assigned_consultation_beds__isnull=True) + | Q(assigned_consultation_beds__end_date__isnull=False) + ).count(), + 1, + )