Skip to content

Commit

Permalink
Add chronic condition api (#2775)
Browse files Browse the repository at this point in the history
Add chronic condition api (#2775)
  • Loading branch information
sainak authored Jan 24, 2025
1 parent 2f9fa83 commit ca3345e
Show file tree
Hide file tree
Showing 16 changed files with 951 additions and 121 deletions.
30 changes: 0 additions & 30 deletions care/abdm/migrations/0014_replace_0013.py

This file was deleted.

46 changes: 0 additions & 46 deletions care/abdm/migrations_old/0013_abhanumber_patient.py

This file was deleted.

2 changes: 1 addition & 1 deletion care/audit_log/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class RequestInformation(NamedTuple):
exception: Exception | None


logger = logging.getLogger(__name__)
logger = logging.getLogger("audit_log")


class AuditLogMiddleware:
Expand Down
1 change: 0 additions & 1 deletion care/emr/api/viewsets/allergy_intolerance.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def authorize_create(self, instance):
"can_write_patient_obj", self.request.user, self.get_patient_obj()
):
raise PermissionDenied("You do not have permission to update encounter")
# TODO If there is an encounter, check access to the encounter

def get_queryset(self):
if not AuthorizationController.call(
Expand Down
11 changes: 6 additions & 5 deletions care/emr/api/viewsets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,14 @@ def perform_update(self, instance):
updated_by=self.request.user,
)

def clean_update_data(self, request_data):
def clean_update_data(self, request_data, keep_fields: set | None = None):
if type(request_data) is list:
return request_data
request_data.pop("id", None)
request_data.pop("external_id", None)
request_data.pop("patient", None)
request_data.pop("encounter", None)
ignored_fields = {"id", "external_id", "patient", "encounter"}
if keep_fields:
ignored_fields = ignored_fields - set(keep_fields)
for field in ignored_fields:
request_data.pop(field, None)
return request_data

def update(self, request, *args, **kwargs):
Expand Down
99 changes: 90 additions & 9 deletions care/emr/api/viewsets/condition.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
from django.shortcuts import get_object_or_404
from django_filters import CharFilter, FilterSet, UUIDFilter
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.generics import get_object_or_404

from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin
from care.emr.api.viewsets.base import (
EMRBaseViewSet,
EMRCreateMixin,
EMRListMixin,
EMRModelViewSet,
EMRQuestionnaireResponseMixin,
EMRRetrieveMixin,
EMRUpdateMixin,
EMRUpsertMixin,
)
from care.emr.api.viewsets.encounter_authz_base import EncounterBasedAuthorizationBase
from care.emr.models.condition import Condition
from care.emr.models.encounter import Encounter
from care.emr.models.patient import Patient
from care.emr.registries.system_questionnaire.system_questionnaire import (
InternalQuestionnaireRegistry,
)
from care.emr.resources.condition.spec import (
CategoryChoices,
ChronicConditionUpdateSpec,
ConditionReadSpec,
ConditionSpec,
ConditionSpecRead,
ConditionSpecUpdate,
ConditionUpdateSpec,
)
from care.emr.resources.questionnaire.spec import SubjectType
from care.security.authorization import AuthorizationController


class ValidateEncounterMixin:
Expand Down Expand Up @@ -54,8 +66,8 @@ class SymptomViewSet(
):
database_model = Condition
pydantic_model = ConditionSpec
pydantic_read_model = ConditionSpecRead
pydantic_update_model = ConditionSpecUpdate
pydantic_read_model = ConditionReadSpec
pydantic_update_model = ConditionUpdateSpec
# Filters
filterset_class = ConditionFilters
filter_backends = [DjangoFilterBackend]
Expand Down Expand Up @@ -94,8 +106,8 @@ class DiagnosisViewSet(
):
database_model = Condition
pydantic_model = ConditionSpec
pydantic_read_model = ConditionSpecRead
pydantic_update_model = ConditionSpecUpdate
pydantic_read_model = ConditionReadSpec
pydantic_update_model = ConditionUpdateSpec

# Filters
filterset_class = ConditionFilters
Expand Down Expand Up @@ -125,3 +137,72 @@ def get_queryset(self):


InternalQuestionnaireRegistry.register(DiagnosisViewSet)


class ChronicConditionViewSet(
EMRQuestionnaireResponseMixin,
EMRCreateMixin,
EMRRetrieveMixin,
EMRUpdateMixin,
EMRListMixin,
EMRBaseViewSet,
EMRUpsertMixin,
):
database_model = Condition
pydantic_model = ConditionSpec
pydantic_read_model = ConditionReadSpec
pydantic_update_model = ChronicConditionUpdateSpec

# Filters
filterset_class = ConditionFilters
filter_backends = [DjangoFilterBackend]
# Questionnaire Spec
questionnaire_type = "chronic_condition"
questionnaire_title = "Chronic Condition"
questionnaire_description = "Chronic Condition"
questionnaire_subject_type = SubjectType.patient.value

def get_patient_obj(self):
return get_object_or_404(
Patient, external_id=self.kwargs["patient_external_id"]
)

def authorize_create(self, instance):
if not AuthorizationController.call(
"can_write_patient_obj", self.request.user, self.get_patient_obj()
):
raise PermissionDenied("You do not have permission to update encounter")

def authorize_update(self, request_obj, model_instance):
encounter = get_object_or_404(Encounter, external_id=request_obj.encounter)
if not AuthorizationController.call(
"can_update_encounter_obj",
self.request.user,
encounter,
):
raise PermissionDenied("You do not have permission to update encounter")

def perform_create(self, instance):
instance.category = CategoryChoices.chronic_condition.value
super().perform_create(instance)

def clean_update_data(self, request_data):
return super().clean_update_data(request_data, keep_fields={"encounter"})

def get_queryset(self):
if not AuthorizationController.call(
"can_view_clinical_data", self.request.user, self.get_patient_obj()
):
raise PermissionDenied("Permission denied for patient data")
return (
super()
.get_queryset()
.filter(
patient__external_id=self.kwargs["patient_external_id"],
category=CategoryChoices.chronic_condition.value,
)
.select_related("patient", "encounter", "created_by", "updated_by")
)


InternalQuestionnaireRegistry.register(ChronicConditionViewSet)
19 changes: 19 additions & 0 deletions care/emr/migrations/0012_alter_condition_encounter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.1.4 on 2025-01-24 07:58

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


class Migration(migrations.Migration):

dependencies = [
('emr', '0011_medicationrequest_requester'),
]

operations = [
migrations.AlterField(
model_name='condition',
name='encounter',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.encounter'),
),
]
4 changes: 3 additions & 1 deletion care/emr/models/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class Condition(EMRBaseModel):
code = models.JSONField(default=dict, null=False, blank=False)
body_site = models.JSONField(default=dict, null=False, blank=False)
patient = models.ForeignKey("emr.Patient", on_delete=models.CASCADE)
encounter = models.ForeignKey("emr.Encounter", on_delete=models.CASCADE)
encounter = models.ForeignKey(
"emr.Encounter", on_delete=models.CASCADE, null=True, blank=True
)
onset = models.JSONField(default=dict)
abatement = models.JSONField(default=dict)
recorded_date = models.DateTimeField(null=True, blank=True)
Expand Down
16 changes: 13 additions & 3 deletions care/emr/resources/condition/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from enum import Enum

from pydantic import UUID4, Field, field_validator
from rest_framework.generics import get_object_or_404

from care.emr.fhir.schema.base import Coding
from care.emr.models.condition import Condition
Expand Down Expand Up @@ -96,7 +97,7 @@ def perform_extra_deserialization(self, is_update, obj):
obj.patient = obj.encounter.patient


class ConditionSpecRead(BaseConditionSpec):
class ConditionReadSpec(BaseConditionSpec):
"""
Validation for deeper models may not be required on read, Just an extra optimisation
"""
Expand All @@ -119,15 +120,16 @@ class ConditionSpecRead(BaseConditionSpec):
@classmethod
def perform_extra_serialization(cls, mapping, obj):
mapping["id"] = obj.external_id
mapping["encounter"] = obj.encounter.external_id
if obj.encounter:
mapping["encounter"] = obj.encounter.external_id

if obj.created_by:
mapping["created_by"] = UserSpec.serialize(obj.created_by)
if obj.updated_by:
mapping["updated_by"] = UserSpec.serialize(obj.updated_by)


class ConditionSpecUpdate(BaseConditionSpec):
class ConditionUpdateSpec(BaseConditionSpec):
clinical_status: ClinicalStatusChoices | None = None
verification_status: VerificationStatusChoices
severity: SeverityChoices | None = None
Expand All @@ -142,3 +144,11 @@ def validate_code(cls, code: int):
return validate_valueset(
"code", cls.model_fields["code"].json_schema_extra["slug"], code
)


class ChronicConditionUpdateSpec(ConditionUpdateSpec):
encounter: UUID4

def perform_extra_deserialization(self, is_update, obj):
if self.encounter:
obj.encounter = get_object_or_404(Encounter, external_id=self.encounter)
5 changes: 5 additions & 0 deletions care/emr/tests/test_booking_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import UTC, datetime, timedelta

from django.test.utils import ignore_warnings
from django.urls import reverse

from care.emr.models import (
Expand All @@ -20,6 +21,7 @@
from config.patient_otp_authentication import PatientOtpObject


@ignore_warnings(category=RuntimeWarning, message=r".*received a naive datetime.*")
class TestBookingViewSet(CareAPITestBase):
def setUp(self):
super().setUp()
Expand Down Expand Up @@ -376,6 +378,7 @@ def test_list_available_users(self):
self.assertGreaterEqual(len(response.data["users"]), 1)


@ignore_warnings(category=RuntimeWarning, message=r".*received a naive datetime.*")
class TestSlotViewSetAppointmentApi(CareAPITestBase):
def setUp(self):
super().setUp()
Expand Down Expand Up @@ -582,6 +585,7 @@ def test_over_booking_a_slot(self):
self.assertContains(response, status_code=400, text="Slot is already full")


@ignore_warnings(category=RuntimeWarning, message=r".*received a naive datetime.*")
class TestSlotViewSetSlotStatsApis(CareAPITestBase):
def setUp(self):
super().setUp()
Expand Down Expand Up @@ -932,6 +936,7 @@ def test_availability_heatmap_slots_same_as_get_slots_for_day_with_exceptions(se
self.assertEqual(slot_stats["total_slots"], total_slots_for_day)


@ignore_warnings(category=RuntimeWarning, message=r".*received a naive datetime.*")
class TestOtpSlotViewSet(CareAPITestBase):
def setUp(self):
super().setUp()
Expand Down
Loading

0 comments on commit ca3345e

Please sign in to comment.