From ce02ab69b84dd90b2a51268dd7de4de2c3323e5c Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 30 May 2024 16:41:25 +0530 Subject: [PATCH 01/14] Adds camera preset model --- care/facility/models/asset.py | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index c85a206e89..fe91860484 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -292,3 +292,65 @@ class AssetServiceEdit(models.Model): class Meta: ordering = ["-edited_on"] + + +CAMERA_PRESET_POSITION_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "z": {"type": "number"}, + }, + "additionalProperties": False, +} + +CAMERA_PRESET_BOUNDARY_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "x0": {"type": "number"}, + "y0": {"type": "number"}, + "x1": {"type": "number"}, + "y1": {"type": "number"}, + }, + "additionalProperties": False, +} + + +class CameraPreset(BaseModel): + name = models.CharField(max_length=255, null=True) + asset_bed = models.ForeignKey( + "facility.AssetBed", on_delete=models.PROTECT, related_name="camera_presets" + ) + + position = models.JSONField( + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)], null=True + ) + boundary = models.JSONField( + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_BOUNDARY_SCHEMA)], null=True + ) + + created_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + updated_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + is_migrated = models.BooleanField(default=False) + + class Meta: + constraints = [ + models.CheckConstraint( + name="position_xor_boundary", + check=( + Q(position__isnull=False, boundary__isnull=True) + | Q(position__isnull=True, boundary__isnull=False) + ), + ), + models.UniqueConstraint( + name="single_boundary_preset_for_assetbed", + fields=("asset_bed",), + condition=Q(boundary__isnull=False, deleted=False), + ), + ] From 9be89c9311fad927273d92282ffeb8abbe4a0890 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 30 May 2024 16:53:01 +0530 Subject: [PATCH 02/14] Migration to backfill and soft delete duplicate asset bed records --- .../facility/migrations/0441_camera_preset.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 care/facility/migrations/0441_camera_preset.py diff --git a/care/facility/migrations/0441_camera_preset.py b/care/facility/migrations/0441_camera_preset.py new file mode 100644 index 0000000000..f71ae30b4e --- /dev/null +++ b/care/facility/migrations/0441_camera_preset.py @@ -0,0 +1,192 @@ +# Generated by Django 4.2.8 on 2024-05-30 06:56 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.core.paginator import Paginator +from django.db import migrations, models +from django.db.models import F, Window +from django.db.models.functions import RowNumber + +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0440_merge_20240528_1613"), + ] + + def backfill_camera_presets_table(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + CameraPreset = apps.get_model("facility", "CameraPreset") + + paginator = Paginator( + AssetBed.objects.annotate( + row_number=Window( + expression=RowNumber(), + partition_by=[F("asset"), F("bed")], + order_by=F("id").asc(), + ) + ) + .filter(deleted=False, asset__asset_class="ONVIF") + .order_by("asset", "bed", "id"), + 1000, + ) + + for page_number in paginator.page_range: + assetbeds_to_delete = [] + presets_to_create = [] + + for asset_bed in paginator.page(page_number).object_list: + name = asset_bed.meta.get("preset_name") + + if position := asset_bed.meta.get("position"): + presets_to_create.append( + CameraPreset( + name=name, + asset_bed=AssetBed.objects.filter( + asset=asset_bed.asset, bed=asset_bed.bed + ).order_by("id")[0], + position={ + "x": position["x"], + "y": position["y"], + "z": position["zoom"], + }, + is_migrated=True, + ) + ) + if asset_bed.row_number != 1: + assetbeds_to_delete.append(asset_bed.id) + else: + assetbeds_to_delete.append(asset_bed.id) + + CameraPreset.objects.bulk_create(presets_to_create) + AssetBed.objects.filter(id__in=assetbeds_to_delete).update( + deleted=True, meta={} + ) + AssetBed.objects.filter(deleted=False, asset__asset_class="ONVIF").update( + meta={} + ) + + operations = [ + migrations.CreateModel( + name="CameraPreset", + 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)), + ("name", models.CharField(max_length=255, null=True)), + ( + "position", + models.JSONField( + null=True, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": False, + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "z": {"type": "number"}, + }, + "type": "object", + } + ) + ], + ), + ), + ( + "boundary", + models.JSONField( + null=True, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": False, + "properties": { + "x0": {"type": "number"}, + "x1": {"type": "number"}, + "y0": {"type": "number"}, + "y1": {"type": "number"}, + }, + "type": "object", + } + ) + ], + ), + ), + ("is_migrated", models.BooleanField(default=False)), + ( + "asset_bed", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="camera_presets", + to="facility.assetbed", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddConstraint( + model_name="camerapreset", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("boundary__isnull", True), ("position__isnull", False)), + models.Q(("boundary__isnull", False), ("position__isnull", True)), + _connector="OR", + ), + name="position_xor_boundary", + ), + ), + migrations.AddConstraint( + model_name="camerapreset", + constraint=models.UniqueConstraint( + condition=models.Q(("boundary__isnull", False), ("deleted", False)), + fields=("asset_bed",), + name="single_boundary_preset_for_assetbed", + ), + ), + migrations.RunPython(backfill_camera_presets_table, migrations.RunPython.noop), + ] From 78750d08a5761b1d885da242d13d51d2598efd96 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 30 May 2024 19:01:24 +0530 Subject: [PATCH 03/14] Delete assed bed records that has no asset class --- .../facility/migrations/0441_camera_preset.py | 23 +++++++++++++++++-- care/facility/models/bed.py | 9 ++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/care/facility/migrations/0441_camera_preset.py b/care/facility/migrations/0441_camera_preset.py index f71ae30b4e..e03ada58df 100644 --- a/care/facility/migrations/0441_camera_preset.py +++ b/care/facility/migrations/0441_camera_preset.py @@ -18,7 +18,11 @@ class Migration(migrations.Migration): ("facility", "0440_merge_20240528_1613"), ] - def backfill_camera_presets_table(apps, schema_editor): + def delete_asset_beds_without_asset_class(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + AssetBed.objects.filter(asset__asset_class__isnull=True).delete() + + def backfill_camera_presets(apps, schema_editor): AssetBed = apps.get_model("facility", "AssetBed") CameraPreset = apps.get_model("facility", "CameraPreset") @@ -188,5 +192,20 @@ def backfill_camera_presets_table(apps, schema_editor): name="single_boundary_preset_for_assetbed", ), ), - migrations.RunPython(backfill_camera_presets_table, migrations.RunPython.noop), + migrations.RunPython( + delete_asset_beds_without_asset_class, + migrations.RunPython.noop, + ), + migrations.RunPython( + backfill_camera_presets, + migrations.RunPython.noop, + ), + migrations.AddConstraint( + model_name="assetbed", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("asset", "bed"), + name="unique_together_asset_bed", + ), + ), ] diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index 1f6ae30777..21da66014e 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -67,6 +67,15 @@ class AssetBed(BaseModel): bed = models.ForeignKey(Bed, on_delete=models.PROTECT, null=False, blank=False) meta = JSONField(default=dict, blank=True) + class Meta: + constraints = [ + models.UniqueConstraint( + name="unique_together_asset_bed", + fields=("asset", "bed"), + condition=models.Q(deleted=False), + ), + ] + def __str__(self): return f"{self.asset.name} - {self.bed.name}" From ae7dfeef5068f06d44c81d74f967c0b1be97eee8 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 5 Jul 2024 17:47:47 +0530 Subject: [PATCH 04/14] rebase migrations --- .../migrations/{0441_camera_preset.py => 0444_camera_preset.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename care/facility/migrations/{0441_camera_preset.py => 0444_camera_preset.py} (98%) diff --git a/care/facility/migrations/0441_camera_preset.py b/care/facility/migrations/0444_camera_preset.py similarity index 98% rename from care/facility/migrations/0441_camera_preset.py rename to care/facility/migrations/0444_camera_preset.py index e03ada58df..9f96b426ff 100644 --- a/care/facility/migrations/0441_camera_preset.py +++ b/care/facility/migrations/0444_camera_preset.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0440_merge_20240528_1613"), + ("facility", "0443_remove_patientconsultation_consent_records_and_more"), ] def delete_asset_beds_without_asset_class(apps, schema_editor): From 0560415d01e46c8107abd9008d578d8e9ba4ca80 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 23 Jul 2024 16:17:36 +0530 Subject: [PATCH 05/14] stash --- .../facility/api/serializers/camera_preset.py | 65 +++++++++++++++++++ care/facility/api/viewsets/camera_preset.py | 40 ++++++++++++ ...amera_preset.py => 0445_camera_presets.py} | 2 +- care/facility/models/__init__.py | 1 + care/facility/models/asset.py | 62 ------------------ care/facility/models/camera_preset.py | 65 +++++++++++++++++++ config/api_router.py | 7 ++ 7 files changed, 179 insertions(+), 63 deletions(-) create mode 100644 care/facility/api/serializers/camera_preset.py create mode 100644 care/facility/api/viewsets/camera_preset.py rename care/facility/migrations/{0444_camera_preset.py => 0445_camera_presets.py} (98%) create mode 100644 care/facility/models/camera_preset.py diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py new file mode 100644 index 0000000000..c19ee2bc10 --- /dev/null +++ b/care/facility/api/serializers/camera_preset.py @@ -0,0 +1,65 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from care.facility.api.serializers.bed import AssetBedSerializer +from care.facility.models import CameraPreset +from care.users.api.serializers.user import UserBaseMinimumSerializer + + +class CameraPresetSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + created_by = UserBaseMinimumSerializer(read_only=True) + updated_by = UserBaseMinimumSerializer(read_only=True) + asset_bed = AssetBedSerializer(read_only=True) + + class Meta: + model = CameraPreset + exclude = ( + "external_id", + "deleted", + ) + read_only_fields = ( + "created_date", + "modified_date", + "is_migrated", + "created_by", + "updated_by", + ) + + def validate(self, attrs): + validated_data = super().validate(attrs) + + asset_bed = ( + self.instance.asset_bed if self.instance else self.context["asset_bed"] + ) + position = validated_data.get( + "position", self.instance and self.instance.position + ) + boundary = validated_data.get( + "boundary", self.instance and self.instance.boundary + ) + + if not self.instance: + # one of position or boundary only must be present + if not (validated_data.get("position") or validated_data.get("boundary")): + raise ValidationError("Either position or boundary must be specified") + + # single boundary preset for an asset_bed + if boundary and CameraPreset.objects.filter(asset_bed=asset_bed).exists(): + raise ValidationError( + "Only one boundary preset can exist for an asset_bed" + ) + # one of position or boundary only must be present + if position and boundary: + raise ValidationError("Cannot have both position and a boundary.") + + return validated_data + + def create(self, validated_data): + validated_data["asset_bed"] = self.context["asset_bed"] + validated_data["created_by"] = self.context["request"].user + return super().create(validated_data) + + def update(self, instance, validated_data): + validated_data["updated_by"] = self.context["request"].user + return super().create(validated_data) diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py new file mode 100644 index 0000000000..8650fd0c60 --- /dev/null +++ b/care/facility/api/viewsets/camera_preset.py @@ -0,0 +1,40 @@ +from django.shortcuts import get_object_or_404 +from django_filters import rest_framework as filters +from dry_rest_permissions.generics import DRYPermissions +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ModelViewSet + +from care.facility.api.serializers.camera_preset import CameraPresetSerializer +from care.facility.models import AssetBed, CameraPreset + + +class CameraPresetFilter(filters.FilterSet): + position = filters.ChoiceFilter(method="filter_preset_type") + boundary = filters.BooleanFilter(method="filter_preset_type") + + def filter_preset_type(self, queryset, name, value): + if value is not None: + return queryset.filter(**{f"${name}__is_null": not value}) + + +class CameraPresetViewSet(ModelViewSet): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all() + lookup_field = "external_id" + permission_classes = (IsAuthenticated, DRYPermissions) + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = CameraPresetFilter + + def get_asset_bed_obj(self): + return get_object_or_404( + AssetBed.objects.filter(external_id=self.kwargs["assetbed_external_id"]) + ) + + def get_queryset(self): + asset_bed = self.get_asset_bed_obj() + return super().get_queryset().filter(asset_bed=asset_bed) + + def get_serializer_context(self): + context = super().get_serializer_context() + context["asset_bed"] = self.get_asset_bed_obj() + return context diff --git a/care/facility/migrations/0444_camera_preset.py b/care/facility/migrations/0445_camera_presets.py similarity index 98% rename from care/facility/migrations/0444_camera_preset.py rename to care/facility/migrations/0445_camera_presets.py index 9f96b426ff..600510540c 100644 --- a/care/facility/migrations/0444_camera_preset.py +++ b/care/facility/migrations/0445_camera_presets.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0443_remove_patientconsultation_consent_records_and_more"), + ("facility", "0444_alter_medicineadministration_dosage_and_more"), ] def delete_asset_beds_without_asset_class(apps, schema_editor): diff --git a/care/facility/models/__init__.py b/care/facility/models/__init__.py index 8993152ef2..69ed17d200 100644 --- a/care/facility/models/__init__.py +++ b/care/facility/models/__init__.py @@ -4,6 +4,7 @@ from .ambulance import * # noqa from .asset import * # noqa from .bed import * # noqa +from .camera_preset import * # noqa from .daily_round import * # noqa from .encounter_symptom import * # noqa from .events import * # noqa diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index fe91860484..c85a206e89 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -292,65 +292,3 @@ class AssetServiceEdit(models.Model): class Meta: ordering = ["-edited_on"] - - -CAMERA_PRESET_POSITION_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "x": {"type": "number"}, - "y": {"type": "number"}, - "z": {"type": "number"}, - }, - "additionalProperties": False, -} - -CAMERA_PRESET_BOUNDARY_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "x0": {"type": "number"}, - "y0": {"type": "number"}, - "x1": {"type": "number"}, - "y1": {"type": "number"}, - }, - "additionalProperties": False, -} - - -class CameraPreset(BaseModel): - name = models.CharField(max_length=255, null=True) - asset_bed = models.ForeignKey( - "facility.AssetBed", on_delete=models.PROTECT, related_name="camera_presets" - ) - - position = models.JSONField( - validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)], null=True - ) - boundary = models.JSONField( - validators=[JSONFieldSchemaValidator(CAMERA_PRESET_BOUNDARY_SCHEMA)], null=True - ) - - created_by = models.ForeignKey( - "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" - ) - updated_by = models.ForeignKey( - "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" - ) - is_migrated = models.BooleanField(default=False) - - class Meta: - constraints = [ - models.CheckConstraint( - name="position_xor_boundary", - check=( - Q(position__isnull=False, boundary__isnull=True) - | Q(position__isnull=True, boundary__isnull=False) - ), - ), - models.UniqueConstraint( - name="single_boundary_preset_for_assetbed", - fields=("asset_bed",), - condition=Q(boundary__isnull=False, deleted=False), - ), - ] diff --git a/care/facility/models/camera_preset.py b/care/facility/models/camera_preset.py new file mode 100644 index 0000000000..265d51b54d --- /dev/null +++ b/care/facility/models/camera_preset.py @@ -0,0 +1,65 @@ +from django.db import models + +from care.utils.models.base import BaseModel +from care.utils.models.validators import JSONFieldSchemaValidator + +CAMERA_PRESET_POSITION_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "z": {"type": "number"}, + }, + "additionalProperties": False, +} + +CAMERA_PRESET_BOUNDARY_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "x0": {"type": "number"}, + "y0": {"type": "number"}, + "x1": {"type": "number"}, + "y1": {"type": "number"}, + }, + "additionalProperties": False, +} + + +class CameraPreset(BaseModel): + name = models.CharField(max_length=255, null=True) + asset_bed = models.ForeignKey( + "facility.AssetBed", on_delete=models.PROTECT, related_name="camera_presets" + ) + + position = models.JSONField( + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)], null=True + ) + boundary = models.JSONField( + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_BOUNDARY_SCHEMA)], null=True + ) + + created_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + updated_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + is_migrated = models.BooleanField(default=False) + + class Meta: + constraints = [ + models.CheckConstraint( + name="position_xor_boundary", + check=( + models.Q(position__isnull=False, boundary__isnull=True) + | models.Q(position__isnull=True, boundary__isnull=False) + ), + ), + models.UniqueConstraint( + name="single_boundary_preset_for_assetbed", + fields=("asset_bed",), + condition=models.Q(boundary__isnull=False, deleted=False), + ), + ] diff --git a/config/api_router.py b/config/api_router.py index 78cb45736b..2c927873e7 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -26,6 +26,7 @@ ConsultationBedViewSet, PatientAssetBedViewSet, ) +from care.facility.api.viewsets.camera_preset import CameraPresetViewSet from care.facility.api.viewsets.consultation_diagnosis import ( ConsultationDiagnosisViewSet, ) @@ -235,6 +236,11 @@ router.register("assetbed", AssetBedViewSet, basename="asset-bed") router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") +assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") +assetbed_nested_router.register( + r"camera-presets", CameraPresetViewSet, basename="assetbed-camera-presets" +) + router.register("patient/search", PatientSearchViewSet, basename="patient-search") router.register("patient", PatientViewSet, basename="patient") patient_nested_router = NestedSimpleRouter(router, r"patient", lookup="patient") @@ -333,6 +339,7 @@ path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), path("", include(asset_nested_router.urls)), + path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), path("", include(consultation_nested_router.urls)), From d5b24ecf4f9383dc808d2afe18384184a96a7d92 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 20 Sep 2024 15:28:42 +0530 Subject: [PATCH 06/14] rebase migrations --- care/facility/api/viewsets/camera_preset.py | 6 +++++- .../{0445_camera_presets.py => 0460_camera_presets.py} | 2 +- config/api_router.py | 7 +------ 3 files changed, 7 insertions(+), 8 deletions(-) rename care/facility/migrations/{0445_camera_presets.py => 0460_camera_presets.py} (99%) diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py index 8650fd0c60..d39f40b0a4 100644 --- a/care/facility/api/viewsets/camera_preset.py +++ b/care/facility/api/viewsets/camera_preset.py @@ -9,9 +9,13 @@ class CameraPresetFilter(filters.FilterSet): - position = filters.ChoiceFilter(method="filter_preset_type") + position = filters.BooleanFilter(method="filter_preset_type") boundary = filters.BooleanFilter(method="filter_preset_type") + asset = filters.UUIDFilter(field_name="asset_bed__asset__external_id") + bed = filters.UUIDFilter(field_name="asset_bed__bed__external_id") + asset_bed = filters.UUIDFilter(field_name="asset_bed__external_id") + def filter_preset_type(self, queryset, name, value): if value is not None: return queryset.filter(**{f"${name}__is_null": not value}) diff --git a/care/facility/migrations/0445_camera_presets.py b/care/facility/migrations/0460_camera_presets.py similarity index 99% rename from care/facility/migrations/0445_camera_presets.py rename to care/facility/migrations/0460_camera_presets.py index 600510540c..5a358cbeaf 100644 --- a/care/facility/migrations/0445_camera_presets.py +++ b/care/facility/migrations/0460_camera_presets.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0444_alter_medicineadministration_dosage_and_more"), + ("facility", "0459_remove_bed_unique_bed_name_per_location_and_more"), ] def delete_asset_beds_without_asset_class(apps, schema_editor): diff --git a/config/api_router.py b/config/api_router.py index a559266730..02666eb54e 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -235,11 +235,7 @@ router.register("bed", BedViewSet, basename="bed") router.register("assetbed", AssetBedViewSet, basename="asset-bed") router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") - -assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") -assetbed_nested_router.register( - r"camera-presets", CameraPresetViewSet, basename="assetbed-camera-presets" -) +router.register("camera_preset", CameraPresetViewSet, basename="camera-preset") router.register("patient/search", PatientSearchViewSet, basename="patient-search") router.register("patient", PatientViewSet, basename="patient") @@ -339,7 +335,6 @@ path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), path("", include(asset_nested_router.urls)), - path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), path("", include(consultation_nested_router.urls)), From aee281aafe595b2b67ef21d64e4f600bfbb1367f Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Sun, 29 Sep 2024 13:10:59 +0530 Subject: [PATCH 07/14] rebase migrations and fix issues --- .../facility/api/serializers/camera_preset.py | 31 +++++++++++---- care/facility/api/viewsets/camera_preset.py | 38 +++++++++++++++---- ...mera_presets.py => 0466_camera_presets.py} | 8 ++-- care/facility/models/bed.py | 4 ++ care/facility/models/camera_preset.py | 4 +- config/api_router.py | 12 +++++- 6 files changed, 76 insertions(+), 21 deletions(-) rename care/facility/migrations/{0460_camera_presets.py => 0466_camera_presets.py} (96%) diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py index c19ee2bc10..fb83aa4613 100644 --- a/care/facility/api/serializers/camera_preset.py +++ b/care/facility/api/serializers/camera_preset.py @@ -26,12 +26,26 @@ class Meta: "updated_by", ) + def get_asset_bed_obj(self): + return self.instance.asset_bed if self.instance else self.context["asset_bed"] + + def validate_name(self, value): + presets_of_related_bed = ( + CameraPreset.objects.filter( + asset_bed__bed_id=self.get_asset_bed_obj().bed_id + ) + .only("name") + .values_list("name") + ) + if value in [x[0] for x in presets_of_related_bed]: + msg = "Name should be unique. Another preset related to this bed already uses the same name." + raise ValidationError(msg) + return value + def validate(self, attrs): validated_data = super().validate(attrs) - asset_bed = ( - self.instance.asset_bed if self.instance else self.context["asset_bed"] - ) + asset_bed = self.get_asset_bed_obj() position = validated_data.get( "position", self.instance and self.instance.position ) @@ -42,16 +56,17 @@ def validate(self, attrs): if not self.instance: # one of position or boundary only must be present if not (validated_data.get("position") or validated_data.get("boundary")): - raise ValidationError("Either position or boundary must be specified") + msg = "Either position or boundary must be specified" + raise ValidationError(msg) # single boundary preset for an asset_bed if boundary and CameraPreset.objects.filter(asset_bed=asset_bed).exists(): - raise ValidationError( - "Only one boundary preset can exist for an asset_bed" - ) + msg = "Only one boundary preset can exist for an asset_bed" + raise ValidationError(msg) # one of position or boundary only must be present if position and boundary: - raise ValidationError("Cannot have both position and a boundary.") + msg = "Cannot have both position and a boundary." + raise ValidationError(msg) return validated_data diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py index d39f40b0a4..5d71ff2e32 100644 --- a/care/facility/api/viewsets/camera_preset.py +++ b/care/facility/api/viewsets/camera_preset.py @@ -1,8 +1,8 @@ from django.shortcuts import get_object_or_404 from django_filters import rest_framework as filters -from dry_rest_permissions.generics import DRYPermissions +from rest_framework.mixins import ListModelMixin from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet from care.facility.api.serializers.camera_preset import CameraPresetSerializer from care.facility.models import AssetBed, CameraPreset @@ -18,16 +18,29 @@ class CameraPresetFilter(filters.FilterSet): def filter_preset_type(self, queryset, name, value): if value is not None: - return queryset.filter(**{f"${name}__is_null": not value}) + return queryset.filter(**{f"{name}__isnull": not value}) + return queryset -class CameraPresetViewSet(ModelViewSet): +class AssetBedCameraPresetFilter(filters.FilterSet): + position = filters.BooleanFilter(method="filter_preset_type") + boundary = filters.BooleanFilter(method="filter_preset_type") + + def filter_preset_type(self, queryset, name, value): + if value is not None: + return queryset.filter(**{f"{name}__isnull": not value}) + return queryset + + +class AssetBedCameraPresetViewSet(ModelViewSet): serializer_class = CameraPresetSerializer - queryset = CameraPreset.objects.all() + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) lookup_field = "external_id" - permission_classes = (IsAuthenticated, DRYPermissions) + permission_classes = (IsAuthenticated,) filter_backends = (filters.DjangoFilterBackend,) - filterset_class = CameraPresetFilter + filterset_class = AssetBedCameraPresetFilter def get_asset_bed_obj(self): return get_object_or_404( @@ -42,3 +55,14 @@ def get_serializer_context(self): context = super().get_serializer_context() context["asset_bed"] = self.get_asset_bed_obj() return context + + +class CameraPresetViewSet(GenericViewSet, ListModelMixin): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) + lookup_field = "external_id" + permission_classes = (IsAuthenticated,) + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = CameraPresetFilter diff --git a/care/facility/migrations/0460_camera_presets.py b/care/facility/migrations/0466_camera_presets.py similarity index 96% rename from care/facility/migrations/0460_camera_presets.py rename to care/facility/migrations/0466_camera_presets.py index 5a358cbeaf..a5a97ddcb4 100644 --- a/care/facility/migrations/0460_camera_presets.py +++ b/care/facility/migrations/0466_camera_presets.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0459_remove_bed_unique_bed_name_per_location_and_more"), + ("facility", "0465_merge_20240923_1045"), ] def delete_asset_beds_without_asset_class(apps, schema_editor): @@ -56,7 +56,7 @@ def backfill_camera_presets(apps, schema_editor): position={ "x": position["x"], "y": position["y"], - "z": position["zoom"], + "zoom": position["zoom"], }, is_migrated=True, ) @@ -113,8 +113,9 @@ def backfill_camera_presets(apps, schema_editor): "properties": { "x": {"type": "number"}, "y": {"type": "number"}, - "z": {"type": "number"}, + "zoom": {"type": "number"}, }, + "required": ["x", "y", "zoom"], "type": "object", } ) @@ -136,6 +137,7 @@ def backfill_camera_presets(apps, schema_editor): "y0": {"type": "number"}, "y1": {"type": "number"}, }, + "required": ["x0", "y0", "x1", "y1"], "type": "object", } ) diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index 7b99fa0290..992f36ac74 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -80,6 +80,10 @@ class Meta: def __str__(self): return f"{self.asset.name} - {self.bed.name}" + def delete(self, *args): + self.camera_presets.update(deleted=True) + return super().delete(*args) + class ConsultationBed(BaseModel): consultation = models.ForeignKey( diff --git a/care/facility/models/camera_preset.py b/care/facility/models/camera_preset.py index 265d51b54d..c680d84bf8 100644 --- a/care/facility/models/camera_preset.py +++ b/care/facility/models/camera_preset.py @@ -9,8 +9,9 @@ "properties": { "x": {"type": "number"}, "y": {"type": "number"}, - "z": {"type": "number"}, + "zoom": {"type": "number"}, }, + "required": ["x", "y", "zoom"], "additionalProperties": False, } @@ -23,6 +24,7 @@ "x1": {"type": "number"}, "y1": {"type": "number"}, }, + "required": ["x0", "y0", "x1", "y1"], "additionalProperties": False, } diff --git a/config/api_router.py b/config/api_router.py index 0e5760e600..27509530fe 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -26,7 +26,10 @@ ConsultationBedViewSet, PatientAssetBedViewSet, ) -from care.facility.api.viewsets.camera_preset import CameraPresetViewSet +from care.facility.api.viewsets.camera_preset import ( + AssetBedCameraPresetViewSet, + CameraPresetViewSet, +) from care.facility.api.viewsets.consultation_diagnosis import ( ConsultationDiagnosisViewSet, ) @@ -235,7 +238,11 @@ router.register("bed", BedViewSet, basename="bed") router.register("assetbed", AssetBedViewSet, basename="asset-bed") router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") -router.register("camera_preset", CameraPresetViewSet, basename="camera-preset") +router.register("camera_presets", CameraPresetViewSet, basename="camera-preset") +assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") +assetbed_nested_router.register( + r"camera_presets", AssetBedCameraPresetViewSet, basename="assetbed-camera-presets" +) router.register("patient/search", PatientSearchViewSet, basename="patient-search") router.register("patient", PatientViewSet, basename="patient") @@ -329,6 +336,7 @@ path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), path("", include(asset_nested_router.urls)), + path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), path("", include(consultation_nested_router.urls)), From a971edde041197d57240e67f96cbb578f9ab45fb Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 3 Oct 2024 15:44:23 +0530 Subject: [PATCH 08/14] fix accidentally creating preset in update preset --- care/facility/api/serializers/camera_preset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py index fb83aa4613..79be56ebed 100644 --- a/care/facility/api/serializers/camera_preset.py +++ b/care/facility/api/serializers/camera_preset.py @@ -68,13 +68,13 @@ def validate(self, attrs): msg = "Cannot have both position and a boundary." raise ValidationError(msg) + validated_data["asset_bed"] = self.get_asset_bed_obj() return validated_data def create(self, validated_data): - validated_data["asset_bed"] = self.context["asset_bed"] validated_data["created_by"] = self.context["request"].user return super().create(validated_data) def update(self, instance, validated_data): validated_data["updated_by"] = self.context["request"].user - return super().create(validated_data) + return super().update(instance, validated_data) From a49350d25029879eecdbcb0e2a8ccb13db830f39 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Mon, 7 Oct 2024 10:32:17 +0530 Subject: [PATCH 09/14] remove boundary preset support --- .../facility/api/serializers/camera_preset.py | 30 +------------ care/facility/api/viewsets/camera_preset.py | 20 --------- .../migrations/0466_camera_presets.py | 42 ------------------- care/facility/models/camera_preset.py | 36 +--------------- 4 files changed, 2 insertions(+), 126 deletions(-) diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py index 79be56ebed..23e6fea6ee 100644 --- a/care/facility/api/serializers/camera_preset.py +++ b/care/facility/api/serializers/camera_preset.py @@ -42,37 +42,9 @@ def validate_name(self, value): raise ValidationError(msg) return value - def validate(self, attrs): - validated_data = super().validate(attrs) - - asset_bed = self.get_asset_bed_obj() - position = validated_data.get( - "position", self.instance and self.instance.position - ) - boundary = validated_data.get( - "boundary", self.instance and self.instance.boundary - ) - - if not self.instance: - # one of position or boundary only must be present - if not (validated_data.get("position") or validated_data.get("boundary")): - msg = "Either position or boundary must be specified" - raise ValidationError(msg) - - # single boundary preset for an asset_bed - if boundary and CameraPreset.objects.filter(asset_bed=asset_bed).exists(): - msg = "Only one boundary preset can exist for an asset_bed" - raise ValidationError(msg) - # one of position or boundary only must be present - if position and boundary: - msg = "Cannot have both position and a boundary." - raise ValidationError(msg) - - validated_data["asset_bed"] = self.get_asset_bed_obj() - return validated_data - def create(self, validated_data): validated_data["created_by"] = self.context["request"].user + validated_data["asset_bed"] = self.get_asset_bed_obj() return super().create(validated_data) def update(self, instance, validated_data): diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py index 5d71ff2e32..079a9f1f0a 100644 --- a/care/facility/api/viewsets/camera_preset.py +++ b/care/facility/api/viewsets/camera_preset.py @@ -9,28 +9,10 @@ class CameraPresetFilter(filters.FilterSet): - position = filters.BooleanFilter(method="filter_preset_type") - boundary = filters.BooleanFilter(method="filter_preset_type") - asset = filters.UUIDFilter(field_name="asset_bed__asset__external_id") bed = filters.UUIDFilter(field_name="asset_bed__bed__external_id") asset_bed = filters.UUIDFilter(field_name="asset_bed__external_id") - def filter_preset_type(self, queryset, name, value): - if value is not None: - return queryset.filter(**{f"{name}__isnull": not value}) - return queryset - - -class AssetBedCameraPresetFilter(filters.FilterSet): - position = filters.BooleanFilter(method="filter_preset_type") - boundary = filters.BooleanFilter(method="filter_preset_type") - - def filter_preset_type(self, queryset, name, value): - if value is not None: - return queryset.filter(**{f"{name}__isnull": not value}) - return queryset - class AssetBedCameraPresetViewSet(ModelViewSet): serializer_class = CameraPresetSerializer @@ -39,8 +21,6 @@ class AssetBedCameraPresetViewSet(ModelViewSet): ) lookup_field = "external_id" permission_classes = (IsAuthenticated,) - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = AssetBedCameraPresetFilter def get_asset_bed_obj(self): return get_object_or_404( diff --git a/care/facility/migrations/0466_camera_presets.py b/care/facility/migrations/0466_camera_presets.py index a5a97ddcb4..317c8b39c0 100644 --- a/care/facility/migrations/0466_camera_presets.py +++ b/care/facility/migrations/0466_camera_presets.py @@ -104,7 +104,6 @@ def backfill_camera_presets(apps, schema_editor): ( "position", models.JSONField( - null=True, validators=[ care.utils.models.validators.JSONFieldSchemaValidator( { @@ -122,28 +121,6 @@ def backfill_camera_presets(apps, schema_editor): ], ), ), - ( - "boundary", - models.JSONField( - null=True, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": False, - "properties": { - "x0": {"type": "number"}, - "x1": {"type": "number"}, - "y0": {"type": "number"}, - "y1": {"type": "number"}, - }, - "required": ["x0", "y0", "x1", "y1"], - "type": "object", - } - ) - ], - ), - ), ("is_migrated", models.BooleanField(default=False)), ( "asset_bed", @@ -175,25 +152,6 @@ def backfill_camera_presets(apps, schema_editor): ), ], ), - migrations.AddConstraint( - model_name="camerapreset", - constraint=models.CheckConstraint( - check=models.Q( - models.Q(("boundary__isnull", True), ("position__isnull", False)), - models.Q(("boundary__isnull", False), ("position__isnull", True)), - _connector="OR", - ), - name="position_xor_boundary", - ), - ), - migrations.AddConstraint( - model_name="camerapreset", - constraint=models.UniqueConstraint( - condition=models.Q(("boundary__isnull", False), ("deleted", False)), - fields=("asset_bed",), - name="single_boundary_preset_for_assetbed", - ), - ), migrations.RunPython( delete_asset_beds_without_asset_class, migrations.RunPython.noop, diff --git a/care/facility/models/camera_preset.py b/care/facility/models/camera_preset.py index c680d84bf8..b1128f8817 100644 --- a/care/facility/models/camera_preset.py +++ b/care/facility/models/camera_preset.py @@ -15,33 +15,15 @@ "additionalProperties": False, } -CAMERA_PRESET_BOUNDARY_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "x0": {"type": "number"}, - "y0": {"type": "number"}, - "x1": {"type": "number"}, - "y1": {"type": "number"}, - }, - "required": ["x0", "y0", "x1", "y1"], - "additionalProperties": False, -} - class CameraPreset(BaseModel): name = models.CharField(max_length=255, null=True) asset_bed = models.ForeignKey( "facility.AssetBed", on_delete=models.PROTECT, related_name="camera_presets" ) - position = models.JSONField( - validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)], null=True - ) - boundary = models.JSONField( - validators=[JSONFieldSchemaValidator(CAMERA_PRESET_BOUNDARY_SCHEMA)], null=True + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)] ) - created_by = models.ForeignKey( "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" ) @@ -49,19 +31,3 @@ class CameraPreset(BaseModel): "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" ) is_migrated = models.BooleanField(default=False) - - class Meta: - constraints = [ - models.CheckConstraint( - name="position_xor_boundary", - check=( - models.Q(position__isnull=False, boundary__isnull=True) - | models.Q(position__isnull=True, boundary__isnull=False) - ), - ), - models.UniqueConstraint( - name="single_boundary_preset_for_assetbed", - fields=("asset_bed",), - condition=models.Q(boundary__isnull=False, deleted=False), - ), - ] From 2375ecdf45cf28c39348334d87f4360efa57c66f Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 8 Oct 2024 15:13:20 +0530 Subject: [PATCH 10/14] optimize preset name valdiation check --------- Co-authored-by: Aakash Singh --- care/facility/api/serializers/camera_preset.py | 15 ++++++--------- care/facility/api/viewsets/camera_preset.py | 11 +++++------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py index 23e6fea6ee..7157b5245a 100644 --- a/care/facility/api/serializers/camera_preset.py +++ b/care/facility/api/serializers/camera_preset.py @@ -27,17 +27,14 @@ class Meta: ) def get_asset_bed_obj(self): - return self.instance.asset_bed if self.instance else self.context["asset_bed"] + return ( + self.instance.asset_bed if self.instance else self.context.get("asset_bed") + ) def validate_name(self, value): - presets_of_related_bed = ( - CameraPreset.objects.filter( - asset_bed__bed_id=self.get_asset_bed_obj().bed_id - ) - .only("name") - .values_list("name") - ) - if value in [x[0] for x in presets_of_related_bed]: + if CameraPreset.objects.filter( + asset_bed__bed_id=self.get_asset_bed_obj().bed_id, name=value + ).exists(): msg = "Name should be unique. Another preset related to this bed already uses the same name." raise ValidationError(msg) return value diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py index 079a9f1f0a..3d23ae2d95 100644 --- a/care/facility/api/viewsets/camera_preset.py +++ b/care/facility/api/viewsets/camera_preset.py @@ -8,12 +8,6 @@ from care.facility.models import AssetBed, CameraPreset -class CameraPresetFilter(filters.FilterSet): - asset = filters.UUIDFilter(field_name="asset_bed__asset__external_id") - bed = filters.UUIDFilter(field_name="asset_bed__bed__external_id") - asset_bed = filters.UUIDFilter(field_name="asset_bed__external_id") - - class AssetBedCameraPresetViewSet(ModelViewSet): serializer_class = CameraPresetSerializer queryset = CameraPreset.objects.all().select_related( @@ -37,6 +31,11 @@ def get_serializer_context(self): return context +class CameraPresetFilter(filters.FilterSet): + asset = filters.UUIDFilter(field_name="asset_bed__asset__external_id") + bed = filters.UUIDFilter(field_name="asset_bed__bed__external_id") + + class CameraPresetViewSet(GenericViewSet, ListModelMixin): serializer_class = CameraPresetSerializer queryset = CameraPreset.objects.all().select_related( From 8042a7f944f8693603ee2e5533e335a68508cd9b Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 8 Oct 2024 17:17:13 +0530 Subject: [PATCH 11/14] refactor viewsets --- care/facility/api/viewsets/camera_preset.py | 73 +++++++++++++++++---- config/api_router.py | 10 ++- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py index 3d23ae2d95..17c9a30f9e 100644 --- a/care/facility/api/viewsets/camera_preset.py +++ b/care/facility/api/viewsets/camera_preset.py @@ -1,11 +1,13 @@ from django.shortcuts import get_object_or_404 -from django_filters import rest_framework as filters +from rest_framework.exceptions import NotFound from rest_framework.mixins import ListModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet, ModelViewSet from care.facility.api.serializers.camera_preset import CameraPresetSerializer -from care.facility.models import AssetBed, CameraPreset +from care.facility.models import Asset, AssetBed, Bed, CameraPreset +from care.users.models import User +from care.utils.cache.cache_allowed_facilities import get_accessible_facilities class AssetBedCameraPresetViewSet(ModelViewSet): @@ -17,13 +19,23 @@ class AssetBedCameraPresetViewSet(ModelViewSet): permission_classes = (IsAuthenticated,) def get_asset_bed_obj(self): - return get_object_or_404( - AssetBed.objects.filter(external_id=self.kwargs["assetbed_external_id"]) + user = self.request.user + queryset = AssetBed.objects.filter( + external_id=self.kwargs["assetbed_external_id"] ) + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(bed__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(bed__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(bed__facility__id__in=allowed_facilities) + return get_object_or_404(queryset) def get_queryset(self): - asset_bed = self.get_asset_bed_obj() - return super().get_queryset().filter(asset_bed=asset_bed) + return super().get_queryset().filter(asset_bed=self.get_asset_bed_obj()) def get_serializer_context(self): context = super().get_serializer_context() @@ -31,11 +43,6 @@ def get_serializer_context(self): return context -class CameraPresetFilter(filters.FilterSet): - asset = filters.UUIDFilter(field_name="asset_bed__asset__external_id") - bed = filters.UUIDFilter(field_name="asset_bed__bed__external_id") - - class CameraPresetViewSet(GenericViewSet, ListModelMixin): serializer_class = CameraPresetSerializer queryset = CameraPreset.objects.all().select_related( @@ -43,5 +50,45 @@ class CameraPresetViewSet(GenericViewSet, ListModelMixin): ) lookup_field = "external_id" permission_classes = (IsAuthenticated,) - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = CameraPresetFilter + + def get_bed_obj(self, external_id: str): + user = self.request.user + queryset = Bed.objects.filter(external_id=external_id) + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(facility__id__in=allowed_facilities) + return get_object_or_404(queryset) + + def get_asset_obj(self, external_id: str): + user = self.request.user + queryset = Asset.objects.filter(external_id=external_id) + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(current_location__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter( + current_location__facility__district=user.district + ) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter( + current_location__facility__id__in=allowed_facilities + ) + return get_object_or_404(queryset) + + def get_queryset(self): + queryset = super().get_queryset() + if asset_external_id := self.kwargs.get("asset_external_id"): + return queryset.filter( + asset_bed__asset=self.get_asset_obj(asset_external_id) + ) + if bed_external_id := self.kwargs.get("bed_external_id"): + return queryset.filter(asset_bed__bed=self.get_bed_obj(bed_external_id)) + raise NotFound diff --git a/config/api_router.py b/config/api_router.py index 27509530fe..b47dff96a0 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -225,6 +225,9 @@ router.register("asset", AssetViewSet, basename="asset") asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") +asset_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="asset-camera-presets" +) asset_nested_router.register( r"availability", AvailabilityViewSet, basename="asset-availability" ) @@ -236,9 +239,13 @@ router.register("asset_transaction", AssetTransactionViewSet) router.register("bed", BedViewSet, basename="bed") +bed_nested_router = NestedSimpleRouter(router, r"bed", lookup="bed") +bed_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="bed-camera-presets" +) + router.register("assetbed", AssetBedViewSet, basename="asset-bed") router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") -router.register("camera_presets", CameraPresetViewSet, basename="camera-preset") assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") assetbed_nested_router.register( r"camera_presets", AssetBedCameraPresetViewSet, basename="assetbed-camera-presets" @@ -336,6 +343,7 @@ path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), path("", include(asset_nested_router.urls)), + path("", include(bed_nested_router.urls)), path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), From 3aa52e8f4213acac521f8522e833d80b1711c406 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 15 Oct 2024 13:01:02 +0530 Subject: [PATCH 12/14] make asset, bed, assetbed get_queryset reusable based on user --- care/facility/api/viewsets/asset.py | 17 +------ care/facility/api/viewsets/bed.py | 48 +++----------------- care/facility/api/viewsets/camera_preset.py | 49 ++++----------------- care/utils/queryset/asset_bed.py | 47 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 96 deletions(-) create mode 100644 care/utils/queryset/asset_bed.py diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 15dd00e2aa..fc66eff4bf 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -62,6 +62,7 @@ from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_queryset from care.utils.queryset.asset_location import get_asset_location_queryset from care.utils.queryset.facility import get_facility_queryset from config.authentication import MiddlewareAuthentication @@ -290,21 +291,7 @@ class AssetViewSet( filterset_class = AssetFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(current_location__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter( - current_location__facility__district=user.district - ) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter( - current_location__facility__id__in=allowed_facilities - ) + queryset = get_asset_queryset(user=self.request.user, queryset=self.queryset) return queryset.annotate( latest_status=Subquery( AvailabilityRecord.objects.filter( diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 336b5f83c2..db9dd6652f 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -30,6 +30,7 @@ from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_bed_queryset, get_bed_queryset inverse_bed_type = inverse_choices(BedTypeChoices) @@ -76,27 +77,14 @@ class BedViewSet( filterset_class = BedFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - - queryset = queryset.annotate( + queryset = self.queryset.annotate( is_occupied=Exists( ConsultationBed.objects.filter( bed__id=OuterRef("id"), end_date__isnull=True ) ) ) - - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(facility__id__in=allowed_facilities) - return queryset + return get_bed_queryset(user=self.request.user, queryset=queryset) @transaction.atomic def create(self, request, *args, **kwargs): @@ -168,18 +156,7 @@ class AssetBedViewSet( lookup_field = "external_id" def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset + return get_asset_bed_queryset(user=self.request.user, queryset=self.queryset) class PatientAssetBedFilter(filters.FilterSet): @@ -212,20 +189,9 @@ class PatientAssetBedViewSet(ListModelMixin, GenericViewSet): ] def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset.filter( - bed__facility__external_id=self.kwargs["facility_external_id"] - ) + return get_asset_bed_queryset( + user=self.request.user, queryset=self.queryset + ).filter(bed__facility__external_id=self.kwargs["facility_external_id"]) class ConsultationBedFilter(filters.FilterSet): diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py index 17c9a30f9e..bfb168834b 100644 --- a/care/facility/api/viewsets/camera_preset.py +++ b/care/facility/api/viewsets/camera_preset.py @@ -5,9 +5,12 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet from care.facility.api.serializers.camera_preset import CameraPresetSerializer -from care.facility.models import Asset, AssetBed, Bed, CameraPreset -from care.users.models import User -from care.utils.cache.cache_allowed_facilities import get_accessible_facilities +from care.facility.models import CameraPreset +from care.utils.queryset.asset_bed import ( + get_asset_bed_queryset, + get_asset_queryset, + get_bed_queryset, +) class AssetBedCameraPresetViewSet(ModelViewSet): @@ -19,19 +22,9 @@ class AssetBedCameraPresetViewSet(ModelViewSet): permission_classes = (IsAuthenticated,) def get_asset_bed_obj(self): - user = self.request.user - queryset = AssetBed.objects.filter( + queryset = get_asset_bed_queryset(self.request.user).filter( external_id=self.kwargs["assetbed_external_id"] ) - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) return get_object_or_404(queryset) def get_queryset(self): @@ -52,35 +45,11 @@ class CameraPresetViewSet(GenericViewSet, ListModelMixin): permission_classes = (IsAuthenticated,) def get_bed_obj(self, external_id: str): - user = self.request.user - queryset = Bed.objects.filter(external_id=external_id) - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(facility__id__in=allowed_facilities) + queryset = get_bed_queryset(self.request.user).filter(external_id=external_id) return get_object_or_404(queryset) def get_asset_obj(self, external_id: str): - user = self.request.user - queryset = Asset.objects.filter(external_id=external_id) - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(current_location__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter( - current_location__facility__district=user.district - ) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter( - current_location__facility__id__in=allowed_facilities - ) + queryset = get_asset_queryset(self.request.user).filter(external_id=external_id) return get_object_or_404(queryset) def get_queryset(self): diff --git a/care/utils/queryset/asset_bed.py b/care/utils/queryset/asset_bed.py new file mode 100644 index 0000000000..472605edb5 --- /dev/null +++ b/care/utils/queryset/asset_bed.py @@ -0,0 +1,47 @@ +from care.facility.models import Asset, AssetBed, Bed +from care.users.models import User +from care.utils.cache.cache_allowed_facilities import get_accessible_facilities + + +def get_asset_bed_queryset(user, queryset=None): + queryset = queryset or AssetBed.objects.all() + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(bed__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(bed__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(bed__facility__id__in=allowed_facilities) + return queryset + + +def get_bed_queryset(user, queryset=None): + queryset = queryset or Bed.objects.all() + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(facility__id__in=allowed_facilities) + return queryset + + +def get_asset_queryset(user, queryset=None): + queryset = queryset or Asset.objects.all() + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(current_location__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(current_location__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter( + current_location__facility__id__in=allowed_facilities + ) + return queryset From a6049ff01b781150faf19734afddb4d9a5465e43 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 15 Oct 2024 13:17:14 +0530 Subject: [PATCH 13/14] prevent accidentally attempting to evaluate queryset early --- care/utils/queryset/asset_bed.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/care/utils/queryset/asset_bed.py b/care/utils/queryset/asset_bed.py index 472605edb5..f9fe8f925e 100644 --- a/care/utils/queryset/asset_bed.py +++ b/care/utils/queryset/asset_bed.py @@ -4,7 +4,7 @@ def get_asset_bed_queryset(user, queryset=None): - queryset = queryset or AssetBed.objects.all() + queryset = AssetBed.objects.all() if queryset is None else queryset if user.is_superuser: pass elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: @@ -18,7 +18,7 @@ def get_asset_bed_queryset(user, queryset=None): def get_bed_queryset(user, queryset=None): - queryset = queryset or Bed.objects.all() + queryset = Bed.objects.all() if queryset is None else queryset if user.is_superuser: pass elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: @@ -32,7 +32,7 @@ def get_bed_queryset(user, queryset=None): def get_asset_queryset(user, queryset=None): - queryset = queryset or Asset.objects.all() + queryset = Asset.objects.all() if queryset is None else queryset if user.is_superuser: pass elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: From 1370725bb28e2a1213b5cbc7ae441435033da3fe Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 16 Oct 2024 17:41:33 +0530 Subject: [PATCH 14/14] migration: skip purging data, handle exceptions; add tests --- care/facility/api/serializers/bed.py | 13 +- .../migrations/0466_camera_presets.py | 36 ++-- care/facility/tests/test_asset_bed_api.py | 183 ++++++++++++++++++ care/utils/tests/test_utils.py | 8 +- 4 files changed, 215 insertions(+), 25 deletions(-) create mode 100644 care/facility/tests/test_asset_bed_api.py diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 41597e186d..508c2f9619 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -123,11 +123,14 @@ def validate(self, attrs): {"asset": "Should be in the same facility as the bed"} ) if ( - asset.asset_class == AssetClasses.HL7MONITOR.name - and AssetBed.objects.filter( - bed=bed, asset__asset_class=asset.asset_class - ).exists() - ): + asset.asset_class + in [ + AssetClasses.HL7MONITOR.name, + AssetClasses.ONVIF.name, + ] + ) 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" diff --git a/care/facility/migrations/0466_camera_presets.py b/care/facility/migrations/0466_camera_presets.py index 317c8b39c0..8ee6942342 100644 --- a/care/facility/migrations/0466_camera_presets.py +++ b/care/facility/migrations/0466_camera_presets.py @@ -47,32 +47,30 @@ def backfill_camera_presets(apps, schema_editor): name = asset_bed.meta.get("preset_name") if position := asset_bed.meta.get("position"): - presets_to_create.append( - CameraPreset( - name=name, - asset_bed=AssetBed.objects.filter( - asset=asset_bed.asset, bed=asset_bed.bed - ).order_by("id")[0], - position={ - "x": position["x"], - "y": position["y"], - "zoom": position["zoom"], - }, - is_migrated=True, + try: + presets_to_create.append( + CameraPreset( + name=name, + asset_bed=AssetBed.objects.filter( + asset=asset_bed.asset, bed=asset_bed.bed + ).order_by("id")[0], + position={ + "x": float(position["x"]), + "y": float(position["y"]), + "zoom": float(position["zoom"]), + }, + is_migrated=True, + ) ) - ) + except: + pass if asset_bed.row_number != 1: assetbeds_to_delete.append(asset_bed.id) else: assetbeds_to_delete.append(asset_bed.id) CameraPreset.objects.bulk_create(presets_to_create) - AssetBed.objects.filter(id__in=assetbeds_to_delete).update( - deleted=True, meta={} - ) - AssetBed.objects.filter(deleted=False, asset__asset_class="ONVIF").update( - meta={} - ) + AssetBed.objects.filter(id__in=assetbeds_to_delete).update(deleted=True) operations = [ migrations.CreateModel( diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py new file mode 100644 index 0000000000..d22aae9bfd --- /dev/null +++ b/care/facility/tests/test_asset_bed_api.py @@ -0,0 +1,183 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.users.models import User +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.tests.test_utils import TestUtils + + +class AssetBedViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset = cls.create_asset(cls.asset_location) + cls.camera_asset = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + + def test_link_disallowed_asset_class_asset_to_bed(self): + data = { + "asset": self.asset.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_link_asset_to_bed_and_attempt_duplicate_linking(self): + data = { + "asset": self.camera_asset.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 same camera to the same bed again. + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + # List asset beds filtered by asset and bed ID and check only 1 result exists + res = self.client.get("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.data["count"], 1) + + +class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.asset2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + cls.asset_bed1 = cls.create_asset_bed(cls.asset1, cls.bed) + cls.asset_bed2 = cls.create_asset_bed(cls.asset2, cls.bed) + + def get_base_url(self, asset_bed_id=None): + return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" + + def test_create_camera_preset_without_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset without position", + "position": {}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_missing_required_keys_in_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": {"key": "value"}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_not_number(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "not a number", + "y": 1, + "zoom": 1, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_values_as_string(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "1", + "y": "1", + "zoom": "1", + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): + asset_bed = self.asset_bed1 + res = self.client.post( + self.get_base_url(asset_bed.external_id), + { + "name": "Preset with proper position", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + preset_external_id = res.data["id"] + + # Check if preset in asset-bed preset list + res = self.client.get(self.get_base_url(asset_bed.external_id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in asset preset list + res = self.client.get( + f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in bed preset list + res = self.client.get( + f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + def test_create_camera_preset_with_same_name_in_same_bed(self): + data = { + "name": "Duplicate Preset Name", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + } + self.client.post( + self.get_base_url(self.asset_bed1.external_id), data, format="json" + ) + res = self.client.post( + self.get_base_url(self.asset_bed2.external_id), data, format="json" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 1f858c7258..91d4ac8d67 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -38,7 +38,7 @@ Ward, ) from care.facility.models.asset import Asset, AssetLocation -from care.facility.models.bed import Bed, ConsultationBed +from care.facility.models.bed import AssetBed, Bed, ConsultationBed from care.facility.models.facility import FacilityUser from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -446,6 +446,12 @@ def create_bed(cls, facility: Facility, location: AssetLocation, **kwargs): data.update(kwargs) return Bed.objects.create(**data) + @classmethod + def create_asset_bed(cls, asset: Asset, bed: Bed, **kwargs): + data = {"asset": asset, "bed": bed} + data.update(kwargs) + return AssetBed.objects.create(**data) + @classmethod def create_consultation_bed( cls,