Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved asset bed relations for camera preset #2387

Merged
merged 24 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ce02ab6
Adds camera preset model
rithviknishad May 30, 2024
9be89c9
Migration to backfill and soft delete duplicate asset bed records
rithviknishad May 30, 2024
78750d0
Delete assed bed records that has no asset class
rithviknishad May 30, 2024
fc04420
Merge branch 'develop' into rithviknishad/feat/camera-presets
rithviknishad Jul 5, 2024
ae7dfee
rebase migrations
rithviknishad Jul 5, 2024
acd2c8e
Merge branch 'develop' into rithviknishad/feat/camera-presets
rithviknishad Jul 9, 2024
0560415
stash
rithviknishad Jul 23, 2024
e296e21
Merge branch 'develop' into rithviknishad/feat/camera-presets
rithviknishad Aug 22, 2024
6da45bf
Merge branch 'develop' into rithviknishad/feat/camera-presets
rithviknishad Aug 30, 2024
e5041ce
Merge branch 'develop' into rithviknishad/feat/camera-presets
rithviknishad Sep 19, 2024
0a199fb
Merge branch 'develop' into rithviknishad/feat/camera-presets
rithviknishad Sep 20, 2024
d5b24ec
rebase migrations
rithviknishad Sep 20, 2024
b27b7e9
Merge branch 'develop' into rithviknishad/feat/camera-presets
sainak Sep 25, 2024
a90848f
Merge branch 'develop' into rithviknishad/feat/camera-presets
rithviknishad Sep 26, 2024
aee281a
rebase migrations and fix issues
rithviknishad Sep 29, 2024
a971edd
fix accidentally creating preset in update preset
rithviknishad Oct 3, 2024
a49350d
remove boundary preset support
rithviknishad Oct 7, 2024
2375ecd
optimize preset name valdiation check
rithviknishad Oct 8, 2024
8042a7f
refactor viewsets
rithviknishad Oct 8, 2024
55b9eb8
Merge branch 'develop' into rithviknishad/feat/camera-presets
nihal467 Oct 11, 2024
3aa52e8
make asset, bed, assetbed get_queryset reusable based on user
rithviknishad Oct 15, 2024
a6049ff
prevent accidentally attempting to evaluate queryset early
rithviknishad Oct 15, 2024
fc41114
Merge remote-tracking branch 'origin' into rithviknishad/feat/camera-…
rithviknishad Oct 16, 2024
1370725
migration: skip purging data, handle exceptions; add tests
rithviknishad Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions care/facility/api/serializers/camera_preset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 get_asset_bed_obj(self):
return (

Check warning on line 30 in care/facility/api/serializers/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/camera_preset.py#L30

Added line #L30 was not covered by tests
self.instance.asset_bed if self.instance else self.context.get("asset_bed")
)

def validate_name(self, value):
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

Check warning on line 40 in care/facility/api/serializers/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/camera_preset.py#L38-L40

Added lines #L38 - L40 were not covered by tests

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)

Check warning on line 45 in care/facility/api/serializers/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/camera_preset.py#L43-L45

Added lines #L43 - L45 were not covered by tests

def update(self, instance, validated_data):
validated_data["updated_by"] = self.context["request"].user
return super().update(instance, validated_data)

Check warning on line 49 in care/facility/api/serializers/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/camera_preset.py#L48-L49

Added lines #L48 - L49 were not covered by tests
17 changes: 2 additions & 15 deletions care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
48 changes: 7 additions & 41 deletions care/facility/api/viewsets/bed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -76,27 +77,14 @@
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):
Expand Down Expand Up @@ -168,18 +156,7 @@
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)

Check warning on line 159 in care/facility/api/viewsets/bed.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/bed.py#L159

Added line #L159 was not covered by tests


class PatientAssetBedFilter(filters.FilterSet):
Expand Down Expand Up @@ -212,20 +189,9 @@
]

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(

Check warning on line 192 in care/facility/api/viewsets/bed.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/bed.py#L192

Added line #L192 was not covered by tests
user=self.request.user, queryset=self.queryset
).filter(bed__facility__external_id=self.kwargs["facility_external_id"])


class ConsultationBedFilter(filters.FilterSet):
Expand Down
63 changes: 63 additions & 0 deletions care/facility/api/viewsets/camera_preset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from django.shortcuts import get_object_or_404
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 CameraPreset
from care.utils.queryset.asset_bed import (
get_asset_bed_queryset,
get_asset_queryset,
get_bed_queryset,
)


class AssetBedCameraPresetViewSet(ModelViewSet):
serializer_class = CameraPresetSerializer
queryset = CameraPreset.objects.all().select_related(
"asset_bed", "created_by", "updated_by"
)
lookup_field = "external_id"
permission_classes = (IsAuthenticated,)

def get_asset_bed_obj(self):
queryset = get_asset_bed_queryset(self.request.user).filter(

Check warning on line 25 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L25

Added line #L25 was not covered by tests
external_id=self.kwargs["assetbed_external_id"]
)
return get_object_or_404(queryset)

Check warning on line 28 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L28

Added line #L28 was not covered by tests

def get_queryset(self):
return super().get_queryset().filter(asset_bed=self.get_asset_bed_obj())

Check warning on line 31 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L31

Added line #L31 was not covered by tests

def get_serializer_context(self):
context = super().get_serializer_context()
context["asset_bed"] = self.get_asset_bed_obj()
return context

Check warning on line 36 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L34-L36

Added lines #L34 - L36 were not covered by tests


class CameraPresetViewSet(GenericViewSet, ListModelMixin):
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved
serializer_class = CameraPresetSerializer
queryset = CameraPreset.objects.all().select_related(
"asset_bed", "created_by", "updated_by"
)
lookup_field = "external_id"
permission_classes = (IsAuthenticated,)

def get_bed_obj(self, external_id: str):
queryset = get_bed_queryset(self.request.user).filter(external_id=external_id)
return get_object_or_404(queryset)

Check warning on line 49 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L48-L49

Added lines #L48 - L49 were not covered by tests

def get_asset_obj(self, external_id: str):
queryset = get_asset_queryset(self.request.user).filter(external_id=external_id)
return get_object_or_404(queryset)

Check warning on line 53 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L52-L53

Added lines #L52 - L53 were not covered by tests

def get_queryset(self):
queryset = super().get_queryset()

Check warning on line 56 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L56

Added line #L56 was not covered by tests
if asset_external_id := self.kwargs.get("asset_external_id"):
return queryset.filter(

Check warning on line 58 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L58

Added line #L58 was not covered by tests
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

Check warning on line 63 in care/facility/api/viewsets/camera_preset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/camera_preset.py#L62-L63

Added lines #L62 - L63 were not covered by tests
171 changes: 171 additions & 0 deletions care/facility/migrations/0466_camera_presets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# 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", "0465_merge_20240923_1045"),
]

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")

paginator = Paginator(
AssetBed.objects.annotate(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have that many assetbed realtions in prod right now ?

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,
)
sainak marked this conversation as resolved.
Show resolved Hide resolved

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"],
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved
"zoom": position["zoom"],
},
is_migrated=True,
)
)
if asset_bed.row_number != 1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is 1 so special ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to preserve the first asset-bed link since the camera preset would be associated to the asset-bed link. So far each old preset had it's own asset-bed record. However, asset-bed records would be now unique together for asset and bed and the newly created preset record would be linked to this asset bed, hence we need to preserve the first asset-bed.

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrations usually delete data later on, deleting data right away is usually not the best approach.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove it from this PR and create separate PR to remove it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate asset-bed records would have to be marked as deleted for the unique constraint to be applied, and for the front-end to know which asset-bed record to link to when the preset is being created.

We can however skip deleting the meta data from these old records so that data is still preserved, just that duplicate asset-bed link records would no longer be accessible by the viewset.

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(
validators=[
care.utils.models.validators.JSONFieldSchemaValidator(
{
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": False,
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"zoom": {"type": "number"},
},
"required": ["x", "y", "zoom"],
"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.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",
),
),
]
Loading
Loading