Skip to content

Commit

Permalink
add consultation specific assets (#1524)
Browse files Browse the repository at this point in the history
  • Loading branch information
sainak authored and Ashesh3 committed Aug 23, 2023
1 parent e3cc7be commit df5f2c5
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 30 deletions.
122 changes: 92 additions & 30 deletions care/facility/api/serializers/bed.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand All @@ -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(
{
Expand All @@ -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:
Expand All @@ -199,6 +234,7 @@ def validate(self, attrs):
raise ValidationError(
{"end_date": "Cannot create conflicting entry"}
)

else:
raise ValidationError(
{
Expand All @@ -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)
33 changes: 33 additions & 0 deletions care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,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):
Expand Down
1 change: 1 addition & 0 deletions care/facility/api/viewsets/bed.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ class ConsultationBedViewSet(
queryset = (
ConsultationBed.objects.all()
.select_related("consultation", "bed")
.prefetch_related("assets")
.order_by("-created_date")
)
serializer_class = ConsultationBedSerializer
Expand Down
4 changes: 4 additions & 0 deletions care/facility/api/viewsets/patient_consultation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
14 changes: 14 additions & 0 deletions care/facility/models/bed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
51 changes: 51 additions & 0 deletions care/facility/tests/test_consultation_bed_asset_api.py
Original file line number Diff line number Diff line change
@@ -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,
)

0 comments on commit df5f2c5

Please sign in to comment.