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

Added Facility Hubs #2135

Merged
merged 26 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7753fb6
added facility hubs
shivankacker May 8, 2024
5f195c6
added tests
shivankacker May 8, 2024
f22e282
remove unnecessary code
shivankacker May 8, 2024
51dcba7
lint
shivankacker May 8, 2024
51951da
Changes made
shivankacker May 10, 2024
a91e5f9
.
shivankacker May 10, 2024
84f5d1b
Merge branch 'develop' of https://github.com/coronasafe/care into fac…
shivankacker May 10, 2024
51540d5
fixed migrations
shivankacker May 10, 2024
61c9f5d
Update care/facility/api/viewsets/facility.py
sainak May 14, 2024
5ed524b
Merge branch 'develop' of https://github.com/coronasafe/care into fac…
shivankacker May 21, 2024
e89629e
fixed migrations
shivankacker May 21, 2024
06793dd
fixed merge conflicts
shivankacker Aug 23, 2024
36a5f88
Merge remote-tracking branch 'origin/develop' into facility-hubs
sainak Aug 24, 2024
356f489
update migrations
sainak Aug 24, 2024
574744a
Merge branch 'develop' of https://github.com/coronasafe/care into pr/…
shivankacker Aug 25, 2024
cc35a41
Merge branch 'develop' of https://github.com/coronasafe/care into pr/…
shivankacker Aug 29, 2024
c1fcf89
Shifted to spokes routing
shivankacker Aug 31, 2024
5ae8602
fixed tesst
shivankacker Aug 31, 2024
c306672
fix merge conflicts
shivankacker Sep 13, 2024
c69567b
added cyclical check in model
shivankacker Sep 17, 2024
ea99f89
fix merge conflicts
shivankacker Sep 20, 2024
71a792b
merge conflict fix
shivankacker Sep 20, 2024
a90da6f
remade migrations
shivankacker Sep 20, 2024
10328ff
fixed merge conflicts
shivankacker Sep 21, 2024
063b197
Added Cyclical check, updated permissions logic
shivankacker Sep 21, 2024
f506f46
fix merge conflicts
shivankacker Sep 21, 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
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 @@
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")

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

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/facility.py#L218

Added line #L218 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/facility.py#L223

Added line #L223 was not covered by tests

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)
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
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 @@
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 @@
CSV_MAKE_PRETTY = {"facility_type": (lambda x: REVERSE_FACILITY_TYPES[x])}


class FacilityHubSpoke(BaseModel, FacilityRelatedPermissionMixin):
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member

Choose a reason for hiding this comment

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

This ensures bi directional uniqueness, but it does not make sense to have cyclical dependences

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the check is already there in serializers if I am understanding you correctly. I have also added it in the model now. Can you please verify if that is what you meant?

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

Check warning on line 349 in care/facility/models/facility.py

View check run for this annotation

Codecov / codecov/patch

care/facility/models/facility.py#L349

Added line #L349 was not covered by tests


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