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

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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 @@ -6,6 +6,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.patient_sample import PatientSample
from care.facility.models.patient_tele_consultation import PatientTeleConsultation

Expand Down Expand Up @@ -92,6 +93,11 @@ class FacilityAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
djangoql_completion_enabled_by_default = True


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


class FacilityStaffAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
autocomplete_fields = ["facility", "staff"]
djangoql_completion_enabled_by_default = True
Expand Down Expand Up @@ -185,6 +191,7 @@ class FacilityUserAdmin(DjangoQLSearchMixin, admin.ModelAdmin, ExportCsvMixin):


admin.site.register(Facility, FacilityAdmin)
admin.site.register(FacilityHubSpoke, FacilityHubSpokeAdmin)
admin.site.register(FacilityStaff, FacilityStaffAdmin)
admin.site.register(FacilityCapacity, FacilityCapacityAdmin)
admin.site.register(FacilityVolunteer, FacilityVolunteerAdmin)
Expand Down
29 changes: 28 additions & 1 deletion care/facility/api/serializers/facility.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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 @@ -13,6 +13,7 @@
WardSerializer,
)
from care.utils.csp.config import BucketType, get_client_config
from care.utils.serializer.external_id_field import ExternalIdSerializerField
from config.serializers import ChoiceField
from config.validators import MiddlewareDomainAddressValidator

Expand Down Expand Up @@ -152,6 +153,32 @@ def create(self, validated_data):
return super().create(validated_data)


class FacilityHubSerializer(serializers.ModelSerializer):
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
id = serializers.UUIDField(source="external_id", read_only=True)
hub = ExternalIdSerializerField(
queryset=Facility.objects.all(), required=True, write_only=True
)
hub_object = FacilityBareMinimumSerializer(read_only=True, source="hub")
spoke_object = FacilityBareMinimumSerializer(read_only=True, source="spoke")

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

def validate(self, data):
data["spoke"] = self.context["facility"]
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
return data


class FacilityImageUploadSerializer(serializers.ModelSerializer):
cover_image = serializers.ImageField(required=True, write_only=True)
read_cover_image_url = serializers.URLField(read_only=True)
Expand Down
44 changes: 35 additions & 9 deletions 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 @@ -12,6 +13,7 @@

from care.facility.api.serializers.facility import (
FacilityBasicInfoSerializer,
FacilityHubSerializer,
FacilityImageUploadSerializer,
FacilitySerializer,
)
Expand All @@ -21,8 +23,9 @@
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 @@ -61,14 +64,7 @@ def filter_queryset(self, request, queryset, view):
return queryset


class FacilityViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
class FacilityViewSet(viewsets.ModelViewSet):
"""Viewset for facility CRUD operations."""

queryset = Facility.objects.all().select_related(
Expand Down Expand Up @@ -183,3 +179,33 @@ class AllFacilityViewSet(
filterset_class = FacilityFilter
lookup_field = "external_id"
search_fields = ["name", "district__name", "state__name"]


class FacilityHubsViewSet(
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
sainak marked this conversation as resolved.
Show resolved Hide resolved
queryset = FacilityHubSpoke.objects.all().select_related("spoke")
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
serializer_class = FacilityHubSerializer
permission_classes = (IsAuthenticated, DRYPermissions)
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
filter_backends = (filters.DjangoFilterBackend, drf_filters.SearchFilter)
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
lookup_field = "external_id"

def get_queryset(self):
return self.queryset.filter(spoke=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
80 changes: 80 additions & 0 deletions care/facility/migrations/0430_facilityhubspoke_facility_hubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Generated by Django 4.2.10 on 2024-05-10 23:42

import uuid

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

import care.facility.models.mixins.permissions.facility


class Migration(migrations.Migration):
dependencies = [
("facility", "0429_double_pain_scale"),
]

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, "Tele ICU Hub")], default=1),
),
(
"hub",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hub_set",
to="facility.facility",
),
),
(
"spoke",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="spoke_set",
to="facility.facility",
),
),
],
options={
"abstract": False,
},
bases=(
models.Model,
care.facility.models.mixins.permissions.facility.FacilityRelatedPermissionMixin,
),
),
migrations.AddField(
model_name="facility",
name="hubs",
field=models.ManyToManyField(
related_name="spokes",
through="facility.FacilityHubSpoke",
to="facility.facility",
),
),
]
38 changes: 38 additions & 0 deletions care/facility/models/facility.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import IntegerChoices
from django.utils.translation import gettext_lazy as _
from multiselectfield import MultiSelectField
from multiselectfield.utils import get_max_length
from simple_history.models import HistoricalRecords
Expand All @@ -12,6 +14,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 @@ -47,6 +50,11 @@
(6, "Blood Bank"),
]


class HubRelationship(IntegerChoices):
TELE_ICU_HUB = 1, _("Tele ICU Hub")
shivankacker marked this conversation as resolved.
Show resolved Hide resolved


ROOM_TYPES.extend(BASE_ROOM_TYPES)

REVERSE_ROOM_TYPES = reverse_choices(ROOM_TYPES)
Expand Down Expand Up @@ -140,6 +148,9 @@ class Facility(FacilityBaseModel, FacilityPermissionMixin):
District, on_delete=models.SET_NULL, null=True, blank=True
)
state = models.ForeignKey(State, on_delete=models.SET_NULL, null=True, blank=True)
hubs = models.ManyToManyField(
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
"self", through="FacilityHubSpoke", symmetrical=False, related_name="spokes"
)

oxygen_capacity = models.IntegerField(default=0)
type_b_cylinders = models.IntegerField(default=0)
Expand Down Expand Up @@ -215,6 +226,33 @@ def save(self, *args, **kwargs) -> None:
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="hub_set")
spoke = models.ForeignKey(
Facility, on_delete=models.CASCADE, related_name="spoke_set"
)
relationship = models.IntegerField(
choices=HubRelationship.choices, default=HubRelationship.TELE_ICU_HUB
)

def save(self, *args, **kwargs):
if self.hub == self.spoke:
raise ValueError("Hub and Spoke cannot be the same")

if (
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
not self.pk
and FacilityHubSpoke.objects.filter(
shivankacker marked this conversation as resolved.
Show resolved Hide resolved
hub=self.spoke, spoke=self.hub, deleted=False
).exists()
):
raise ValueError("Hub and Spoke already exists")

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
56 changes: 56 additions & 0 deletions care/facility/tests/test_facility_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import status
from rest_framework.test import APITestCase

from care.facility.models.facility import FacilityHubSpoke
from care.utils.tests.test_utils import TestUtils


Expand Down Expand Up @@ -91,3 +92,58 @@ def test_delete_with_active_patients(self):
self.client.force_authenticate(user=state_admin)
response = self.client.delete(f"/api/v1/facility/{facility.external_id}/")
self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_add_hubs(self):
facility = self.create_facility(self.super_user, self.district, self.local_body)
facility2 = self.create_facility(
self.super_user, self.district, self.local_body
)

state_admin = self.create_user("state_admin", self.district, user_type=40)
self.client.force_authenticate(user=state_admin)
response = self.client.post(
f"/api/v1/facility/{facility.external_id}/hubs/",
{"hub": facility2.external_id},
)
self.assertIs(response.status_code, status.HTTP_201_CREATED)

def test_delete_hub(self):
facility = self.create_facility(self.super_user, self.district, self.local_body)
facility2 = self.create_facility(
self.super_user, self.district, self.local_body
)

state_admin = self.create_user("state_admin", self.district, user_type=40)

hub = FacilityHubSpoke.objects.create(hub=facility2, spoke=facility)
self.client.force_authenticate(user=state_admin)
response = self.client.delete(
f"/api/v1/facility/{facility.external_id}/hubs/{hub.external_id}/"
)
self.assertIs(response.status_code, status.HTTP_204_NO_CONTENT)

def test_add_hub_no_permission(self):
facility = self.create_facility(self.super_user, self.district, self.local_body)
facility2 = self.create_facility(
self.super_user, self.district, self.local_body
)

self.client.force_authenticate(user=self.user)
response = self.client.post(
f"/api/v1/facility/{facility.external_id}/hubs/",
{"hub": facility2.external_id},
)
self.assertIs(response.status_code, status.HTTP_403_FORBIDDEN)

def test_delete_hub_no_permission(self):
facility = self.create_facility(self.super_user, self.district, self.local_body)
facility2 = self.create_facility(
self.super_user, self.district, self.local_body
)

hub = FacilityHubSpoke.objects.create(hub=facility2, spoke=facility)
self.client.force_authenticate(user=self.user)
response = self.client.delete(
f"/api/v1/facility/{facility.external_id}/hubs/{hub.external_id}/"
)
self.assertIs(response.status_code, status.HTTP_403_FORBIDDEN)
7 changes: 6 additions & 1 deletion config/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
EventTypeViewSet,
PatientConsultationEventViewSet,
)
from care.facility.api.viewsets.facility import AllFacilityViewSet, FacilityViewSet
from care.facility.api.viewsets.facility import (
AllFacilityViewSet,
FacilityHubsViewSet,
FacilityViewSet,
)
from care.facility.api.viewsets.facility_capacity import FacilityCapacityViewSet
from care.facility.api.viewsets.facility_users import FacilityUserViewSet
from care.facility.api.viewsets.file_upload import FileUploadViewSet
Expand Down Expand Up @@ -189,6 +193,7 @@
facility_nested_router.register(r"inventorysummary", FacilityInventorySummaryViewSet)
facility_nested_router.register(r"min_quantity", FacilityInventoryMinQuantityViewSet)
facility_nested_router.register(r"asset_location", AssetLocationViewSet)
facility_nested_router.register(r"hubs", FacilityHubsViewSet)

facility_location_nested_router = NestedSimpleRouter(
facility_nested_router, r"asset_location", lookup="asset_location"
Expand Down
Loading