Skip to content

Commit

Permalink
Added Facility Hubs (#2135)
Browse files Browse the repository at this point in the history
Added Facility Hubs (#2135)

---------

Co-authored-by: Aakash Singh <[email protected]>
  • Loading branch information
shivankacker and sainak authored Sep 22, 2024
1 parent d4f9208 commit d9f0287
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 3 deletions.
7 changes: 7 additions & 0 deletions care/facility/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from care.facility.models.ambulance import Ambulance, AmbulanceDriver
from care.facility.models.asset import Asset
from care.facility.models.bed import AssetBed, Bed
from care.facility.models.facility import FacilityHubSpoke
from care.facility.models.file_upload import FileUpload
from care.facility.models.patient_consultation import (
PatientConsent,
Expand Down Expand Up @@ -101,6 +102,12 @@ class FacilityAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
djangoql_completion_enabled_by_default = True


@admin.register(FacilityHubSpoke)
class FacilityHubSpokeAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
search_fields = ["name"]
djangoql_completion_enabled_by_default = True


@admin.register(FacilityStaff)
class FacilityStaffAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
autocomplete_fields = ["facility", "staff"]
Expand Down
49 changes: 48 additions & 1 deletion care/facility/api/serializers/facility.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import boto3
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q
from rest_framework import serializers

from care.facility.models import FACILITY_TYPES, Facility, FacilityLocalGovtBody
from care.facility.models.bed import Bed
from care.facility.models.facility import FEATURE_CHOICES
from care.facility.models.facility import FEATURE_CHOICES, FacilityHubSpoke
from care.facility.models.patient import PatientRegistration
from care.users.api.serializers.lsg import (
DistrictSerializer,
Expand All @@ -20,6 +21,7 @@
cover_image_validator,
custom_image_extension_validator,
)
from care.utils.serializer.external_id_field import ExternalIdSerializerField
from config.serializers import ChoiceField
from config.validators import MiddlewareDomainAddressValidator

Expand Down Expand Up @@ -178,6 +180,51 @@ def create(self, validated_data):
return super().create(validated_data)


class FacilitySpokeSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source="external_id", read_only=True)
spoke = ExternalIdSerializerField(
queryset=Facility.objects.all(), required=True, write_only=True
)
hub_object = FacilityBasicInfoSerializer(read_only=True, source="hub")
spoke_object = FacilityBasicInfoSerializer(read_only=True, source="spoke")

class Meta:
model = FacilityHubSpoke
fields = (
"id",
"spoke",
"hub_object",
"spoke_object",
"relationship",
"created_date",
"modified_date",
)
read_only_fields = (
"id",
"spoke_object",
"hub_object",
"created_date",
"modified_date",
)

def validate(self, data):
data["hub"] = self.context["facility"]
return data

def validate_spoke(self, spoke: Facility):
hub: Facility = self.context["facility"]

if hub == spoke:
raise serializers.ValidationError("Cannot set a facility as it's own spoke")

if FacilityHubSpoke.objects.filter(
Q(hub=hub, spoke=spoke) | Q(hub=spoke, spoke=hub)
).first():
raise serializers.ValidationError("Facility is already a spoke/hub")

return spoke


class FacilityImageUploadSerializer(serializers.ModelSerializer):
cover_image = serializers.ImageField(
required=True,
Expand Down
27 changes: 26 additions & 1 deletion care/facility/api/viewsets/facility.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.conf import settings
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from djqscsv import render_to_csv_response
from drf_spectacular.utils import extend_schema, extend_schema_view
Expand All @@ -14,15 +15,17 @@
FacilityBasicInfoSerializer,
FacilityImageUploadSerializer,
FacilitySerializer,
FacilitySpokeSerializer,
)
from care.facility.models import (
Facility,
FacilityCapacity,
FacilityPatientStatsHistory,
HospitalDoctors,
)
from care.facility.models.facility import FacilityUser
from care.facility.models.facility import FacilityHubSpoke, FacilityUser
from care.users.models import User
from care.utils.queryset.facility import get_facility_queryset


class FacilityFilter(filters.FilterSet):
Expand Down Expand Up @@ -183,3 +186,25 @@ class AllFacilityViewSet(
filterset_class = FacilityFilter
lookup_field = "external_id"
search_fields = ["name", "district__name", "state__name"]


class FacilitySpokesViewSet(viewsets.ModelViewSet):
queryset = FacilityHubSpoke.objects.all().select_related("spoke", "hub")
serializer_class = FacilitySpokeSerializer
permission_classes = (IsAuthenticated, DRYPermissions)
lookup_field = "external_id"

def get_queryset(self):
return self.queryset.filter(hub=self.get_facility())

def get_facility(self):
facilities = get_facility_queryset(self.request.user)
return get_object_or_404(
facilities.filter(external_id=self.kwargs["facility_external_id"])
)

def get_serializer_context(self):
facility = self.get_facility()
context = super().get_serializer_context()
context["facility"] = facility
return context
83 changes: 83 additions & 0 deletions care/facility/migrations/0462_facilityhubspoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Generated by Django 5.1.1 on 2024-09-21 12:26

import uuid

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("facility", "0461_remove_patientconsultation_prescriptions_and_more"),
]

operations = [
migrations.CreateModel(
name="FacilityHubSpoke",
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)),
(
"relationship",
models.IntegerField(
choices=[(1, "Regular Hub"), (2, "Tele ICU Hub")], default=1
),
),
(
"hub",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="spokes",
to="facility.facility",
),
),
(
"spoke",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hubs",
to="facility.facility",
),
),
],
options={
"constraints": [
models.CheckConstraint(
condition=models.Q(("hub", models.F("spoke")), _negated=True),
name="hub_and_spoke_not_same",
),
models.UniqueConstraint(
condition=models.Q(("deleted", False)),
fields=("hub", "spoke"),
name="unique_hub_spoke",
),
models.UniqueConstraint(
condition=models.Q(("deleted", False)),
fields=("spoke", "hub"),
name="unique_spoke_hub",
),
],
},
),
]
56 changes: 56 additions & 0 deletions care/facility/models/facility.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import IntegerChoices
from django.db.models.constraints import CheckConstraint, UniqueConstraint
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from simple_history.models import HistoricalRecords

from care.facility.models import FacilityBaseModel, reverse_choices
Expand All @@ -12,6 +16,7 @@
FacilityRelatedPermissionMixin,
)
from care.users.models import District, LocalBody, State, Ward
from care.utils.models.base import BaseModel
from care.utils.models.validators import mobile_or_landline_number_validator

User = get_user_model()
Expand Down Expand Up @@ -49,6 +54,11 @@
]


class HubRelationship(IntegerChoices):
REGULAR_HUB = 1, _("Regular Hub")
TELE_ICU_HUB = 2, _("Tele ICU Hub")


class FacilityFeature(models.IntegerChoices):
CT_SCAN_FACILITY = 1, "CT Scan Facility"
MATERNITY_CARE = 2, "Maternity Care"
Expand Down Expand Up @@ -182,6 +192,17 @@ class FacilityFeature(models.IntegerChoices):
REVERSE_FEATURE_CHOICES = reverse_choices(FEATURE_CHOICES)


# making sure A -> B -> C -> A does not happen
def check_if_spoke_is_not_ancestor(base_id: int, spoke_id: int):
ancestors_of_base = FacilityHubSpoke.objects.filter(spoke_id=base_id).values_list(
"hub_id", flat=True
)
if spoke_id in ancestors_of_base:
raise serializers.ValidationError("This facility is already an ancestor hub")
for ancestor in ancestors_of_base:
check_if_spoke_is_not_ancestor(ancestor, spoke_id)


class Facility(FacilityBaseModel, FacilityPermissionMixin):
name = models.CharField(max_length=1000, blank=False, null=False)
is_active = models.BooleanField(default=True)
Expand Down Expand Up @@ -293,6 +314,41 @@ def get_facility_flags(self):
CSV_MAKE_PRETTY = {"facility_type": (lambda x: REVERSE_FACILITY_TYPES[x])}


class FacilityHubSpoke(BaseModel, FacilityRelatedPermissionMixin):
hub = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="spokes")
spoke = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="hubs")
relationship = models.IntegerField(
choices=HubRelationship.choices, default=HubRelationship.REGULAR_HUB
)

class Meta:
constraints = [
# Ensure hub and spoke are not the same
CheckConstraint(
check=~models.Q(hub=models.F("spoke")),
name="hub_and_spoke_not_same",
),
# bidirectional uniqueness
UniqueConstraint(
fields=["hub", "spoke"],
name="unique_hub_spoke",
condition=models.Q(deleted=False),
),
UniqueConstraint(
fields=["spoke", "hub"],
name="unique_spoke_hub",
condition=models.Q(deleted=False),
),
]

def save(self, *args, **kwargs):
check_if_spoke_is_not_ancestor(self.hub.id, self.spoke.id)
return super().save(*args, **kwargs)

def __str__(self):
return f"Hub: {self.hub.name} Spoke: {self.spoke.name}"


class FacilityLocalGovtBody(models.Model):
"""
DEPRECATED_FROM: 2020-03-29
Expand Down
Loading

0 comments on commit d9f0287

Please sign in to comment.