Skip to content

Commit

Permalink
Symptoms table (#2186)
Browse files Browse the repository at this point in the history
* consultation depth backend

Co-authored-by: Rithvik Nishad <[email protected]>
Co-authored-by: Aakash Singh <[email protected]>

* refactor

* fix migrations

* fix test and dummy data

* add is_migrated field

* add created by to symptoms bulk create

* fix discharge summary

* make onset date non nullable

* fixes unknown field excluded

* fix tests

* fix validations

* update bulk migration to exclude symptom if already created earlier for a consultation

* add clinical_impression_status to indicate symptom status

* update migrations

* review suggestions

* add trigger for marked as errors

* fix validation

* fix updates

* rename consultation symptom to encounter symptom

* fix unable to mark as entered in error

* update discharge summary pdf

* add test cases and minor fixes

* allow create symptoms to be empty

* update migration to ignore asymptomatic symptom

* rebase migrations

---------

Co-authored-by: Hritesh Shanty <[email protected]>
Co-authored-by: Rithvik Nishad <[email protected]>
Co-authored-by: rithviknishad <[email protected]>
  • Loading branch information
4 people authored May 28, 2024
1 parent f051cab commit de22042
Show file tree
Hide file tree
Showing 20 changed files with 1,154 additions and 160 deletions.
5 changes: 1 addition & 4 deletions care/facility/api/serializers/daily_round.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from care.facility.models.bed import Bed
from care.facility.models.daily_round import DailyRound
from care.facility.models.notification import Notification
from care.facility.models.patient_base import SYMPTOM_CHOICES, SuggestionChoices
from care.facility.models.patient_base import SuggestionChoices
from care.facility.models.patient_consultation import PatientConsultation
from care.users.api.serializers.user import UserBaseMinimumSerializer
from care.utils.notification_handler import NotificationGenerator
Expand All @@ -24,9 +24,6 @@

class DailyRoundSerializer(serializers.ModelSerializer):
id = serializers.CharField(source="external_id", read_only=True)
additional_symptoms = serializers.MultipleChoiceField(
choices=SYMPTOM_CHOICES, required=False
)
deprecated_covid_category = ChoiceField(
choices=COVID_CATEGORY_CHOICES, required=False
) # Deprecated
Expand Down
126 changes: 126 additions & 0 deletions care/facility/api/serializers/encounter_symptom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from copy import copy

from django.db import transaction
from django.utils.timezone import now
from rest_framework import serializers

from care.facility.events.handler import create_consultation_events
from care.facility.models.encounter_symptom import (
ClinicalImpressionStatus,
EncounterSymptom,
Symptom,
)
from care.users.api.serializers.user import UserBaseMinimumSerializer


class EncounterSymptomSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source="external_id", read_only=True)
created_by = UserBaseMinimumSerializer(read_only=True)
updated_by = UserBaseMinimumSerializer(read_only=True)

class Meta:
model = EncounterSymptom
exclude = (
"consultation",
"external_id",
"deleted",
)
read_only_fields = (
"created_date",
"modified_date",
"is_migrated",
)

def validate_onset_date(self, value):
if value and value > now():
raise serializers.ValidationError("Onset date cannot be in the future")
return value

def validate(self, attrs):
validated_data = super().validate(attrs)
consultation = (
self.instance.consultation
if self.instance
else self.context["consultation"]
)

onset_date = (
self.instance.onset_date
if self.instance
else validated_data.get("onset_date")
)
if cure_date := validated_data.get("cure_date"):
if cure_date < onset_date:
raise serializers.ValidationError(
{"cure_date": "Cure date should be after onset date"}
)

if validated_data.get("symptom") != Symptom.OTHERS and validated_data.get(
"other_symptom"
):
raise serializers.ValidationError(
{
"other_symptom": "Other symptom should be empty when symptom type is not OTHERS"
}
)

if validated_data.get("symptom") == Symptom.OTHERS and not validated_data.get(
"other_symptom"
):
raise serializers.ValidationError(
{
"other_symptom": "Other symptom should not be empty when symptom type is OTHERS"
}
)

if EncounterSymptom.objects.filter(
consultation=consultation,
symptom=validated_data.get("symptom"),
other_symptom=validated_data.get("other_symptom") or "",
cure_date__isnull=True,
clinical_impression_status=ClinicalImpressionStatus.IN_PROGRESS,
).exists():
raise serializers.ValidationError(
{"symptom": "An active symptom with the same details already exists"}
)

return validated_data

def create(self, validated_data):
validated_data["consultation"] = self.context["consultation"]
validated_data["created_by"] = self.context["request"].user

with transaction.atomic():
instance: EncounterSymptom = super().create(validated_data)

create_consultation_events(
instance.consultation_id,
instance,
instance.created_by_id,
instance.created_date,
)

return instance

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

with transaction.atomic():
old_instance = copy(instance)
instance = super().update(instance, validated_data)

create_consultation_events(
instance.consultation_id,
instance,
instance.updated_by_id,
instance.modified_date,
old=old_instance,
)

return instance


class EncounterCreateSymptomSerializer(serializers.ModelSerializer):
class Meta:
model = EncounterSymptom
fields = ("symptom", "other_symptom", "onset_date", "cure_date")
77 changes: 73 additions & 4 deletions care/facility/api/serializers/patient_consultation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
ConsultationDiagnosisSerializer,
)
from care.facility.api.serializers.daily_round import DailyRoundSerializer
from care.facility.api.serializers.encounter_symptom import (
EncounterCreateSymptomSerializer,
EncounterSymptomSerializer,
)
from care.facility.api.serializers.facility import FacilityBasicInfoSerializer
from care.facility.events.handler import create_consultation_events
from care.facility.models import (
Expand All @@ -32,13 +36,17 @@
)
from care.facility.models.asset import AssetLocation
from care.facility.models.bed import Bed, ConsultationBed
from care.facility.models.encounter_symptom import (
ClinicalImpressionStatus,
EncounterSymptom,
Symptom,
)
from care.facility.models.icd11_diagnosis import (
ConditionVerificationStatus,
ConsultationDiagnosis,
)
from care.facility.models.notification import Notification
from care.facility.models.patient_base import (
SYMPTOM_CHOICES,
NewDischargeReasonEnum,
RouteToFacility,
SuggestionChoices,
Expand Down Expand Up @@ -66,7 +74,6 @@ class PatientConsultationSerializer(serializers.ModelSerializer):
source="suggestion",
)

symptoms = serializers.MultipleChoiceField(choices=SYMPTOM_CHOICES)
deprecated_covid_category = ChoiceField(
choices=COVID_CATEGORY_CHOICES, required=False
)
Expand Down Expand Up @@ -151,7 +158,13 @@ class PatientConsultationSerializer(serializers.ModelSerializer):
help_text="Bulk create diagnoses for the consultation upon creation",
)
diagnoses = ConsultationDiagnosisSerializer(many=True, read_only=True)

create_symptoms = EncounterCreateSymptomSerializer(
many=True,
write_only=True,
required=False,
help_text="Bulk create symptoms for the consultation upon creation",
)
symptoms = EncounterSymptomSerializer(many=True, read_only=True)
medico_legal_case = serializers.BooleanField(default=False, required=False)

def get_discharge_prescription(self, consultation):
Expand Down Expand Up @@ -332,6 +345,7 @@ def create(self, validated_data):
raise ValidationError({"route_to_facility": "This field is required"})

create_diagnosis = validated_data.pop("create_diagnoses")
create_symptoms = validated_data.pop("create_symptoms")
action = -1
review_interval = -1
if "action" in validated_data:
Expand Down Expand Up @@ -407,6 +421,19 @@ def create(self, validated_data):
]
)

symptoms = EncounterSymptom.objects.bulk_create(
EncounterSymptom(
consultation=consultation,
symptom=obj.get("symptom"),
onset_date=obj.get("onset_date"),
cure_date=obj.get("cure_date"),
clinical_impression_status=obj.get("clinical_impression_status"),
other_symptom=obj.get("other_symptom") or "",
created_by=self.context["request"].user,
)
for obj in create_symptoms
)

if bed and consultation.suggestion == SuggestionChoices.A:
consultation_bed = ConsultationBed(
bed=bed,
Expand Down Expand Up @@ -444,7 +471,7 @@ def create(self, validated_data):

create_consultation_events(
consultation.id,
(consultation, *diagnosis),
(consultation, *diagnosis, *symptoms),
consultation.created_by.id,
consultation.created_date,
)
Expand Down Expand Up @@ -502,6 +529,45 @@ def validate_create_diagnoses(self, value):

return value

def validate_create_symptoms(self, value):
if self.instance:
raise ValidationError("Bulk create symptoms is not allowed on update")

counter: set[int | str] = set()
for obj in value:
item: int | str = obj["symptom"]
if obj["symptom"] == Symptom.OTHERS:
other_symptom = obj.get("other_symptom")
if not other_symptom:
raise ValidationError(
{
"other_symptom": "Other symptom should not be empty when symptom type is OTHERS"
}
)
item: str = other_symptom.strip().lower()
if item in counter:
# Reject if duplicate symptoms are provided
raise ValidationError("Duplicate symptoms are not allowed")
counter.add(item)

current_time = now()
for obj in value:
if obj["onset_date"] > current_time:
raise ValidationError(
{"onset_date": "Onset date cannot be in the future"}
)

if cure_date := obj.get("cure_date"):
if cure_date < obj["onset_date"]:
raise ValidationError(
{"cure_date": "Cure date should be after onset date"}
)
obj["clinical_impression_status"] = ClinicalImpressionStatus.COMPLETED
else:
obj["clinical_impression_status"] = ClinicalImpressionStatus.IN_PROGRESS

return value

def validate_encounter_date(self, value):
if value < MIN_ENCOUNTER_DATE:
raise ValidationError(
Expand Down Expand Up @@ -623,6 +689,9 @@ def validate(self, attrs):
if not self.instance and "create_diagnoses" not in validated:
raise ValidationError({"create_diagnoses": ["This field is required."]})

if not self.instance and "create_symptoms" not in validated:
raise ValidationError({"create_symptoms": ["This field is required."]})

return validated


Expand Down
4 changes: 2 additions & 2 deletions care/facility/api/serializers/patient_icmr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework import serializers

from care.facility.models import DISEASE_CHOICES, SAMPLE_TYPE_CHOICES, SYMPTOM_CHOICES
from care.facility.models import DISEASE_CHOICES, SAMPLE_TYPE_CHOICES
from care.facility.models.patient_icmr import (
PatientConsultationICMR,
PatientIcmr,
Expand Down Expand Up @@ -124,7 +124,7 @@ class Meta:

class ICMRMedicalConditionSerializer(serializers.ModelSerializer):
date_of_onset_of_symptoms = serializers.DateField()
symptoms = serializers.ListSerializer(child=ChoiceField(choices=SYMPTOM_CHOICES))
symptoms = serializers.ListSerializer(child=serializers.CharField())
hospitalization_date = serializers.DateField()
hospital_phone_number = serializers.CharField(
source="consultation.facility.phone_number"
Expand Down
57 changes: 57 additions & 0 deletions care/facility/api/viewsets/encounter_symptom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from dry_rest_permissions.generics import DRYPermissions
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet

from care.facility.api.serializers.encounter_symptom import EncounterSymptomSerializer
from care.facility.models.encounter_symptom import (
ClinicalImpressionStatus,
EncounterSymptom,
)
from care.utils.queryset.consultation import get_consultation_queryset


class EncounterSymptomFilter(filters.FilterSet):
is_cured = filters.BooleanFilter(method="filter_is_cured")

def filter_is_cured(self, queryset, name, value):
if value:
return queryset.filter(cure_date__isnull=False)
return queryset.filter(cure_date__isnull=True)


class EncounterSymptomViewSet(ModelViewSet):
serializer_class = EncounterSymptomSerializer
permission_classes = (IsAuthenticated, DRYPermissions)
queryset = EncounterSymptom.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = EncounterSymptomFilter
lookup_field = "external_id"

def get_consultation_obj(self):
return get_object_or_404(
get_consultation_queryset(self.request.user).filter(
external_id=self.kwargs["consultation_external_id"]
)
)

def get_queryset(self):
consultation = self.get_consultation_obj()
return self.queryset.filter(consultation_id=consultation.id)

def get_serializer_context(self):
context = super().get_serializer_context()
context["consultation"] = self.get_consultation_obj()
return context

def perform_destroy(self, instance):
serializer = self.get_serializer(
instance,
data={
"clinical_impression_status": ClinicalImpressionStatus.ENTERED_IN_ERROR
},
partial=True,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
4 changes: 0 additions & 4 deletions care/facility/api/viewsets/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,6 @@ def filter_by_category(self, queryset, name, value):
last_consultation_discharge_date = filters.DateFromToRangeFilter(
field_name="last_consultation__discharge_date"
)
last_consultation_symptoms_onset_date = filters.DateFromToRangeFilter(
field_name="last_consultation__symptoms_onset_date"
)
last_consultation_admitted_bed_type_list = MultiSelectFilter(
method="filter_by_bed_type",
)
Expand Down Expand Up @@ -449,7 +446,6 @@ class PatientViewSet(
"last_vaccinated_date",
"last_consultation_encounter_date",
"last_consultation_discharge_date",
"last_consultation_symptoms_onset_date",
]
CSV_EXPORT_LIMIT = 7

Expand Down
Loading

0 comments on commit de22042

Please sign in to comment.