From f051cabeadf880f02ca79491d3260afef8a6fb04 Mon Sep 17 00:00:00 2001
From: Aakash Singh
Date: Sat, 25 May 2024 19:24:36 +0530
Subject: [PATCH 1/8] bump docker healthcheck retries (#2192)
---
docker/dev.Dockerfile | 2 +-
docker/prod.Dockerfile | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile
index b4eb2f8c92..6cc595fa0a 100644
--- a/docker/dev.Dockerfile
+++ b/docker/dev.Dockerfile
@@ -27,7 +27,7 @@ HEALTHCHECK \
--interval=10s \
--timeout=5s \
--start-period=10s \
- --retries=24 \
+ --retries=48 \
CMD ["/app/scripts/healthcheck.sh"]
WORKDIR /app
diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile
index ab6d548a73..0e89d39c17 100644
--- a/docker/prod.Dockerfile
+++ b/docker/prod.Dockerfile
@@ -57,7 +57,7 @@ HEALTHCHECK \
--interval=30s \
--timeout=5s \
--start-period=10s \
- --retries=6 \
+ --retries=12 \
CMD ["/app/healthcheck.sh"]
COPY . ${APP_HOME}
From de22042e23741c8725dead138cd611405c3520a9 Mon Sep 17 00:00:00 2001
From: Aakash Singh
Date: Tue, 28 May 2024 16:08:40 +0530
Subject: [PATCH 2/8] Symptoms table (#2186)
* consultation depth backend
Co-authored-by: Rithvik Nishad
Co-authored-by: Aakash Singh
* 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
Co-authored-by: Rithvik Nishad
Co-authored-by: rithviknishad
---
care/facility/api/serializers/daily_round.py | 5 +-
.../api/serializers/encounter_symptom.py | 126 +++++
.../api/serializers/patient_consultation.py | 77 +++-
care/facility/api/serializers/patient_icmr.py | 4 +-
.../api/viewsets/encounter_symptom.py | 57 +++
care/facility/api/viewsets/patient.py | 4 -
.../management/commands/load_event_types.py | 25 +-
.../migrations/0439_encounter_symptoms.py | 263 +++++++++++
care/facility/models/__init__.py | 1 +
care/facility/models/daily_round.py | 6 +-
care/facility/models/encounter_symptom.py | 94 ++++
care/facility/models/patient_consultation.py | 18 +-
care/facility/models/patient_icmr.py | 21 +-
.../tests/test_encounter_symptom_api.py | 431 ++++++++++++++++++
.../tests/test_patient_consultation_api.py | 19 +-
.../utils/reports/discharge_summary.py | 6 +
.../patient_discharge_summary_pdf.html | 63 ++-
care/utils/tests/test_utils.py | 14 +-
config/api_router.py | 2 +
data/dummy/facility.json | 78 ----
20 files changed, 1154 insertions(+), 160 deletions(-)
create mode 100644 care/facility/api/serializers/encounter_symptom.py
create mode 100644 care/facility/api/viewsets/encounter_symptom.py
create mode 100644 care/facility/migrations/0439_encounter_symptoms.py
create mode 100644 care/facility/models/encounter_symptom.py
create mode 100644 care/facility/tests/test_encounter_symptom_api.py
diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py
index 0ec6bda9af..eea3e21d61 100644
--- a/care/facility/api/serializers/daily_round.py
+++ b/care/facility/api/serializers/daily_round.py
@@ -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
@@ -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
diff --git a/care/facility/api/serializers/encounter_symptom.py b/care/facility/api/serializers/encounter_symptom.py
new file mode 100644
index 0000000000..858ab7f9c8
--- /dev/null
+++ b/care/facility/api/serializers/encounter_symptom.py
@@ -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")
diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py
index ad0cd8ece3..e08a80ea78 100644
--- a/care/facility/api/serializers/patient_consultation.py
+++ b/care/facility/api/serializers/patient_consultation.py
@@ -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 (
@@ -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,
@@ -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
)
@@ -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):
@@ -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:
@@ -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,
@@ -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,
)
@@ -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(
@@ -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
diff --git a/care/facility/api/serializers/patient_icmr.py b/care/facility/api/serializers/patient_icmr.py
index e252ea4631..b90b5645dc 100644
--- a/care/facility/api/serializers/patient_icmr.py
+++ b/care/facility/api/serializers/patient_icmr.py
@@ -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,
@@ -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"
diff --git a/care/facility/api/viewsets/encounter_symptom.py b/care/facility/api/viewsets/encounter_symptom.py
new file mode 100644
index 0000000000..3f49cef2de
--- /dev/null
+++ b/care/facility/api/viewsets/encounter_symptom.py
@@ -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)
diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py
index a79360d76c..36748513ec 100644
--- a/care/facility/api/viewsets/patient.py
+++ b/care/facility/api/viewsets/patient.py
@@ -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",
)
@@ -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
diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py
index f0cc50d186..7e5d689f48 100644
--- a/care/facility/management/commands/load_event_types.py
+++ b/care/facility/management/commands/load_event_types.py
@@ -33,14 +33,6 @@ class Command(BaseCommand):
{
"name": "CLINICAL",
"children": (
- {
- "name": "SYMPTOMS",
- "fields": (
- "symptoms",
- "other_symptoms",
- "symptoms_onset_date",
- ),
- },
{
"name": "DEATH",
"fields": ("death_datetime", "death_confirmed_doctor"),
@@ -108,10 +100,6 @@ class Command(BaseCommand):
"review_after",
),
"children": (
- {
- "name": "ROUND_SYMPTOMS", # todo resolve clash with consultation symptoms
- "fields": ("additional_symptoms",),
- },
{
"name": "PHYSICAL_EXAMINATION",
"fields": ("physical_examination_info",),
@@ -239,12 +227,25 @@ class Command(BaseCommand):
"model": "ConsultationDiagnosis",
"fields": ("diagnosis", "verification_status", "is_principal"),
},
+ {
+ "name": "SYMPTOMS",
+ "model": "EncounterSymptom",
+ "fields": (
+ "symptom",
+ "other_symptom",
+ "onset_date",
+ "cure_date",
+ "clinical_impression_status",
+ ),
+ },
)
inactive_event_types: Tuple[str, ...] = (
"RESPIRATORY",
"INTAKE_OUTPUT",
"VENTILATOR_MODES",
+ "SYMPTOMS",
+ "ROUND_SYMPTOMS",
"TREATING_PHYSICIAN",
)
diff --git a/care/facility/migrations/0439_encounter_symptoms.py b/care/facility/migrations/0439_encounter_symptoms.py
new file mode 100644
index 0000000000..67f9b17f45
--- /dev/null
+++ b/care/facility/migrations/0439_encounter_symptoms.py
@@ -0,0 +1,263 @@
+# Generated by Django 4.2.10 on 2024-05-17 10:52
+
+import uuid
+
+import django.db.models.deletion
+from django.conf import settings
+from django.core.paginator import Paginator
+from django.db import migrations, models
+
+import care.facility.models.mixins.permissions.patient
+
+
+def backfill_symptoms_table(apps, schema_editor):
+ EncounterSymptom = apps.get_model("facility", "EncounterSymptom")
+ PatientConsultation = apps.get_model("facility", "PatientConsultation")
+ DailyRound = apps.get_model("facility", "DailyRound")
+
+ paginator = Paginator(PatientConsultation.objects.all().order_by("id"), 100)
+ for page_number in paginator.page_range:
+ bulk = []
+ for consultation in paginator.page(page_number).object_list:
+ consultation_symptoms_set = set()
+ for symptom in consultation.deprecated_symptoms:
+ try:
+ symptom_id = int(symptom)
+ if symptom_id == 1:
+ # Asymptomatic
+ continue
+ if symptom_id == 9:
+ # Other symptom
+ if not consultation.deprecated_other_symptoms:
+ # invalid other symptom
+ continue
+ consultation_symptoms_set.add(
+ consultation.deprecated_other_symptoms.lower()
+ )
+ else:
+ consultation_symptoms_set.add(symptom_id)
+ bulk.append(
+ EncounterSymptom(
+ symptom=symptom_id,
+ other_symptom=consultation.deprecated_other_symptoms
+ if symptom_id == 9 # Other symptom
+ else "",
+ onset_date=consultation.deprecated_symptoms_onset_date
+ or consultation.encounter_date,
+ created_date=consultation.created_date,
+ created_by=consultation.created_by,
+ consultation=consultation,
+ is_migrated=True,
+ )
+ )
+ except ValueError:
+ print(
+ f"Invalid Symptom {symptom} for Consultation {consultation.id}"
+ )
+
+ for daily_round in DailyRound.objects.filter(consultation=consultation):
+ for symptom in daily_round.deprecated_additional_symptoms:
+ try:
+ symptom_id = int(symptom)
+ if symptom_id == 1:
+ # Asymptomatic
+ continue
+ if symptom_id == 9:
+ # Other symptom
+ if not daily_round.deprecated_other_symptoms:
+ # invalid other symptom
+ continue
+ if (
+ daily_round.deprecated_other_symptoms.lower()
+ in consultation_symptoms_set
+ ):
+ # Skip if symptom already exists
+ continue
+ consultation_symptoms_set.add(
+ daily_round.deprecated_other_symptoms.lower()
+ )
+ elif symptom_id in consultation_symptoms_set:
+ # Skip if symptom already exists
+ continue
+ else:
+ consultation_symptoms_set.add(symptom_id)
+
+ bulk.append(
+ EncounterSymptom(
+ symptom=symptom_id,
+ other_symptom=daily_round.deprecated_other_symptoms
+ if symptom_id == 9 # Other symptom
+ else "",
+ onset_date=daily_round.created_date,
+ created_date=daily_round.created_date,
+ created_by=daily_round.created_by,
+ consultation=daily_round.consultation,
+ is_migrated=True,
+ )
+ )
+ except ValueError:
+ print(
+ f"Invalid Symptom {symptom} for DailyRound {daily_round.id}"
+ )
+ EncounterSymptom.objects.bulk_create(bulk)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("facility", "0438_alter_dailyround_patient_category_and_more"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="dailyround",
+ old_name="additional_symptoms",
+ new_name="deprecated_additional_symptoms",
+ ),
+ migrations.RenameField(
+ model_name="dailyround",
+ old_name="other_symptoms",
+ new_name="deprecated_other_symptoms",
+ ),
+ migrations.RenameField(
+ model_name="patientconsultation",
+ old_name="other_symptoms",
+ new_name="deprecated_other_symptoms",
+ ),
+ migrations.RenameField(
+ model_name="patientconsultation",
+ old_name="symptoms",
+ new_name="deprecated_symptoms",
+ ),
+ migrations.RenameField(
+ model_name="patientconsultation",
+ old_name="symptoms_onset_date",
+ new_name="deprecated_symptoms_onset_date",
+ ),
+ migrations.CreateModel(
+ name="EncounterSymptom",
+ 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)),
+ (
+ "symptom",
+ models.SmallIntegerField(
+ choices=[
+ (9, "Others"),
+ (2, "Fever"),
+ (3, "Sore Throat"),
+ (4, "Cough"),
+ (5, "Breathlessness"),
+ (6, "Myalgia"),
+ (7, "Abdominal Discomfort"),
+ (8, "Vomiting"),
+ (11, "Sputum"),
+ (12, "Nausea"),
+ (13, "Chest Pain"),
+ (14, "Hemoptysis"),
+ (15, "Nasal Discharge"),
+ (16, "Body Ache"),
+ (17, "Diarrhoea"),
+ (18, "Pain"),
+ (19, "Pedal Edema"),
+ (20, "Wound"),
+ (21, "Constipation"),
+ (22, "Headache"),
+ (23, "Bleeding"),
+ (24, "Dizziness"),
+ (25, "Chills"),
+ (26, "General Weakness"),
+ (27, "Irritability"),
+ (28, "Confusion"),
+ (29, "Abdominal Pain"),
+ (30, "Joint Pain"),
+ (31, "Redness Of Eyes"),
+ (32, "Anorexia"),
+ (33, "New Loss Of Taste"),
+ (34, "New Loss Of Smell"),
+ ]
+ ),
+ ),
+ ("other_symptom", models.CharField(blank=True, default="")),
+ ("onset_date", models.DateTimeField(null=False, blank=False)),
+ ("cure_date", models.DateTimeField(blank=True, null=True)),
+ (
+ "clinical_impression_status",
+ models.CharField(
+ choices=[
+ ("in-progress", "In Progress"),
+ ("completed", "Completed"),
+ ("entered-in-error", "Entered in Error"),
+ ],
+ default="in-progress",
+ max_length=255,
+ ),
+ ),
+ (
+ "consultation",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="symptoms",
+ to="facility.patientconsultation",
+ ),
+ ),
+ (
+ "created_by",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="+",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "updated_by",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="+",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "is_migrated",
+ models.BooleanField(
+ default=False,
+ help_text="This field is to throw caution to data that was previously ported over",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=(
+ models.Model,
+ care.facility.models.mixins.permissions.patient.ConsultationRelatedPermissionMixin,
+ ),
+ ),
+ migrations.RunPython(backfill_symptoms_table, migrations.RunPython.noop),
+ ]
diff --git a/care/facility/models/__init__.py b/care/facility/models/__init__.py
index de348c5fad..f5f21da821 100644
--- a/care/facility/models/__init__.py
+++ b/care/facility/models/__init__.py
@@ -5,6 +5,7 @@
from .asset import * # noqa
from .bed import * # noqa
from .daily_round import * # noqa
+from .encounter_symptom import * # noqa
from .events import * # noqa
from .facility import * # noqa
from .icd11_diagnosis import * # noqa
diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py
index afb05147fb..4b2457e50c 100644
--- a/care/facility/models/daily_round.py
+++ b/care/facility/models/daily_round.py
@@ -141,14 +141,14 @@ class InsulinIntakeFrequencyType(enum.Enum):
max_digits=4, decimal_places=2, blank=True, null=True, default=None
)
physical_examination_info = models.TextField(null=True, blank=True)
- additional_symptoms = MultiSelectField(
+ deprecated_additional_symptoms = MultiSelectField(
choices=SYMPTOM_CHOICES,
default=1,
null=True,
blank=True,
max_length=get_max_length(SYMPTOM_CHOICES, None),
- )
- other_symptoms = models.TextField(default="", blank=True)
+ ) # Deprecated
+ deprecated_other_symptoms = models.TextField(default="", blank=True) # Deprecated
deprecated_covid_category = models.CharField(
choices=COVID_CATEGORY_CHOICES,
max_length=8,
diff --git a/care/facility/models/encounter_symptom.py b/care/facility/models/encounter_symptom.py
new file mode 100644
index 0000000000..1322decbf1
--- /dev/null
+++ b/care/facility/models/encounter_symptom.py
@@ -0,0 +1,94 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from care.facility.models.mixins.permissions.patient import (
+ ConsultationRelatedPermissionMixin,
+)
+from care.facility.models.patient_consultation import PatientConsultation
+from care.utils.models.base import BaseModel
+
+
+class ClinicalImpressionStatus(models.TextChoices):
+ """
+ See: https://fhir-ru.github.io/valueset-clinicalimpression-status.html
+ """
+
+ IN_PROGRESS = "in-progress", _("In Progress")
+ COMPLETED = "completed", _("Completed")
+ ENTERED_IN_ERROR = "entered-in-error", _("Entered in Error")
+
+
+class Symptom(models.IntegerChoices):
+ OTHERS = 9
+ FEVER = 2
+ SORE_THROAT = 3
+ COUGH = 4
+ BREATHLESSNESS = 5
+ MYALGIA = 6
+ ABDOMINAL_DISCOMFORT = 7
+ VOMITING = 8
+ SPUTUM = 11
+ NAUSEA = 12
+ CHEST_PAIN = 13
+ HEMOPTYSIS = 14
+ NASAL_DISCHARGE = 15
+ BODY_ACHE = 16
+ DIARRHOEA = 17
+ PAIN = 18
+ PEDAL_EDEMA = 19
+ WOUND = 20
+ CONSTIPATION = 21
+ HEADACHE = 22
+ BLEEDING = 23
+ DIZZINESS = 24
+ CHILLS = 25
+ GENERAL_WEAKNESS = 26
+ IRRITABILITY = 27
+ CONFUSION = 28
+ ABDOMINAL_PAIN = 29
+ JOINT_PAIN = 30
+ REDNESS_OF_EYES = 31
+ ANOREXIA = 32
+ NEW_LOSS_OF_TASTE = 33
+ NEW_LOSS_OF_SMELL = 34
+
+
+class EncounterSymptom(BaseModel, ConsultationRelatedPermissionMixin):
+ symptom = models.SmallIntegerField(choices=Symptom.choices, null=False, blank=False)
+ other_symptom = models.CharField(default="", blank=True, null=False)
+ onset_date = models.DateTimeField(null=False, blank=False)
+ cure_date = models.DateTimeField(null=True, blank=True)
+ clinical_impression_status = models.CharField(
+ max_length=255,
+ choices=ClinicalImpressionStatus.choices,
+ default=ClinicalImpressionStatus.IN_PROGRESS,
+ )
+ consultation = models.ForeignKey(
+ PatientConsultation,
+ null=True,
+ blank=True,
+ on_delete=models.PROTECT,
+ related_name="symptoms",
+ )
+ created_by = models.ForeignKey(
+ "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+"
+ )
+ updated_by = models.ForeignKey(
+ "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+"
+ )
+ is_migrated = models.BooleanField(
+ default=False,
+ help_text="This field is to throw caution to data that was previously ported over",
+ )
+
+ def save(self, *args, **kwargs):
+ if self.other_symptom and self.symptom != Symptom.OTHERS:
+ raise ValueError("Other Symptom should be empty when Symptom is not OTHERS")
+
+ if self.clinical_impression_status != ClinicalImpressionStatus.ENTERED_IN_ERROR:
+ if self.onset_date and self.cure_date:
+ self.clinical_impression_status = ClinicalImpressionStatus.COMPLETED
+ elif self.onset_date and not self.cure_date:
+ self.clinical_impression_status = ClinicalImpressionStatus.IN_PROGRESS
+
+ super().save(*args, **kwargs)
diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py
index 91dad91de4..1e787d7885 100644
--- a/care/facility/models/patient_consultation.py
+++ b/care/facility/models/patient_consultation.py
@@ -72,15 +72,17 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin):
deprecated_icd11_principal_diagnosis = models.CharField(
max_length=100, default="", blank=True, null=True
) # Deprecated in favour of ConsultationDiagnosis M2M model
- symptoms = MultiSelectField(
+ deprecated_symptoms = MultiSelectField(
choices=SYMPTOM_CHOICES,
default=1,
null=True,
blank=True,
max_length=get_max_length(SYMPTOM_CHOICES, None),
- )
- other_symptoms = models.TextField(default="", blank=True)
- symptoms_onset_date = models.DateTimeField(null=True, blank=True)
+ ) # Deprecated
+ deprecated_other_symptoms = models.TextField(default="", blank=True) # Deprecated
+ deprecated_symptoms_onset_date = models.DateTimeField(
+ null=True, blank=True
+ ) # Deprecated
deprecated_covid_category = models.CharField(
choices=COVID_CATEGORY_CHOICES,
max_length=8,
@@ -256,8 +258,8 @@ def get_related_consultation(self):
CSV_MAPPING = {
"consultation_created_date": "Date of Consultation",
"encounter_date": "Date of Admission",
- "symptoms_onset_date": "Date of Onset of Symptoms",
- "symptoms": "Symptoms at time of consultation",
+ "deprecated_symptoms_onset_date": "Date of Onset of Symptoms",
+ "deprecated_symptoms": "Symptoms at time of consultation",
"deprecated_covid_category": "Covid Category",
"category": "Category",
"examination_details": "Examination Details",
@@ -276,8 +278,8 @@ def get_related_consultation(self):
# CSV_DATATYPE_DEFAULT_MAPPING = {
# "encounter_date": (None, models.DateTimeField(),),
- # "symptoms_onset_date": (None, models.DateTimeField(),),
- # "symptoms": ("-", models.CharField(),),
+ # "deprecated_symptoms_onset_date": (None, models.DateTimeField(),),
+ # "deprecated_symptoms": ("-", models.CharField(),),
# "category": ("-", models.CharField(),),
# "examination_details": ("-", models.CharField(),),
# "suggestion": ("-", models.CharField(),),
diff --git a/care/facility/models/patient_icmr.py b/care/facility/models/patient_icmr.py
index 677b278322..e6f06da451 100644
--- a/care/facility/models/patient_icmr.py
+++ b/care/facility/models/patient_icmr.py
@@ -10,6 +10,7 @@
PatientContactDetails,
PatientRegistration,
PatientSample,
+ Symptom,
)
@@ -187,19 +188,19 @@ def medical_conditions_list(self):
@property
def symptoms(self):
- return [
- symptom
- for symptom in self.consultation.symptoms
- # if SYMPTOM_CHOICES[0][0] not in self.consultation.symptoms.choices.keys()
- ]
+ symptoms = []
+ for symptom in self.consultation.symptoms:
+ if symptom == Symptom.OTHERS:
+ symptoms.append(self.consultation.other_symptoms)
+ else:
+ symptoms.append(symptom)
+
+ return symptoms
@property
def date_of_onset_of_symptoms(self):
- return (
- self.consultation.symptoms_onset_date.date()
- if self.consultation and self.consultation.symptoms_onset_date
- else None
- )
+ if symptom := self.consultation.symptoms.first():
+ return symptom.onset_date.date()
class PatientConsultationICMR(PatientConsultation):
diff --git a/care/facility/tests/test_encounter_symptom_api.py b/care/facility/tests/test_encounter_symptom_api.py
new file mode 100644
index 0000000000..efff7fb6d2
--- /dev/null
+++ b/care/facility/tests/test_encounter_symptom_api.py
@@ -0,0 +1,431 @@
+from datetime import timedelta
+
+from django.utils.timezone import now
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from care.facility.models.encounter_symptom import (
+ ClinicalImpressionStatus,
+ EncounterSymptom,
+ Symptom,
+)
+from care.facility.models.icd11_diagnosis import (
+ ConditionVerificationStatus,
+ ICD11Diagnosis,
+)
+from care.utils.tests.test_utils import TestUtils
+
+
+class TestEncounterSymptomInConsultation(TestUtils, APITestCase):
+ @classmethod
+ def setUpTestData(cls) -> None:
+ cls.state = cls.create_state()
+ cls.district = cls.create_district(cls.state)
+ cls.local_body = cls.create_local_body(cls.district)
+ cls.super_user = cls.create_super_user("su", cls.district)
+ cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body)
+ cls.location = cls.create_asset_location(cls.facility)
+ cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility)
+ cls.doctor = cls.create_user(
+ "doctor", cls.district, home_facility=cls.facility, user_type=15
+ )
+ cls.patient = cls.create_patient(cls.district, cls.facility)
+ cls.consultation_data = cls.get_consultation_data()
+ cls.consultation_data.update(
+ {
+ "patient": cls.patient.external_id,
+ "consultation": cls.facility.external_id,
+ "treating_physician": cls.doctor.id,
+ "create_diagnoses": [
+ {
+ "diagnosis": ICD11Diagnosis.objects.first().id,
+ "is_principal": False,
+ "verification_status": ConditionVerificationStatus.CONFIRMED,
+ }
+ ],
+ "create_symptoms": [
+ {
+ "symptom": Symptom.COUGH,
+ "onset_date": now(),
+ },
+ {
+ "symptom": Symptom.FEVER,
+ "onset_date": now(),
+ },
+ ],
+ }
+ )
+
+ def test_create_consultation(self):
+ data = self.consultation_data.copy()
+
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(EncounterSymptom.objects.count(), 2)
+
+ def test_create_consultation_with_duplicate_symptoms(self):
+ data = self.consultation_data.copy()
+ data["create_symptoms"] = [
+ {
+ "symptom": Symptom.FEVER,
+ "onset_date": now(),
+ },
+ {
+ "symptom": Symptom.FEVER,
+ "onset_date": now(),
+ },
+ ]
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"create_symptoms": ["Duplicate symptoms are not allowed"]},
+ )
+
+ data["create_symptoms"] = [
+ {
+ "symptom": Symptom.OTHERS,
+ "other_symptom": "Other Symptom",
+ "onset_date": now(),
+ },
+ {
+ "symptom": Symptom.OTHERS,
+ "other_symptom": "Other Symptom",
+ "onset_date": now(),
+ },
+ ]
+
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"create_symptoms": ["Duplicate symptoms are not allowed"]},
+ )
+
+ def test_create_consultation_with_no_symptom(self):
+ data = self.consultation_data.copy()
+
+ data["create_symptoms"] = []
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(EncounterSymptom.objects.count(), 0)
+
+ def test_create_consultation_with_invalid_symptom(self):
+ data = self.consultation_data.copy()
+ data["create_symptoms"] = [
+ {
+ "symptom": 100,
+ "onset_date": now(),
+ },
+ ]
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"create_symptoms": [{"symptom": ['"100" is not a valid choice.']}]},
+ )
+
+ data["create_symptoms"] = [
+ {
+ "symptom": Symptom.OTHERS,
+ "onset_date": now(),
+ },
+ ]
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {
+ "create_symptoms": {
+ "other_symptom": "Other symptom should not be empty when symptom type is OTHERS"
+ }
+ },
+ )
+
+ def test_create_consultation_with_no_symptom_onset_date(self):
+ data = self.consultation_data.copy()
+ data["create_symptoms"] = [
+ {
+ "symptom": Symptom.FEVER,
+ },
+ ]
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"create_symptoms": [{"onset_date": ["This field is required."]}]},
+ )
+
+ def test_create_consultation_with_symptom_onset_date_in_future(self):
+ data = self.consultation_data.copy()
+ data["create_symptoms"] = [
+ {
+ "symptom": Symptom.FEVER,
+ "onset_date": now() + timedelta(days=1),
+ },
+ ]
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"create_symptoms": {"onset_date": "Onset date cannot be in the future"}},
+ )
+
+ def test_create_consultation_with_cure_date_before_onset_date(self):
+ data = self.consultation_data.copy()
+ data["create_symptoms"] = [
+ {
+ "symptom": Symptom.FEVER,
+ "onset_date": now(),
+ "cure_date": now() - timedelta(days=1),
+ },
+ ]
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"create_symptoms": {"cure_date": "Cure date should be after onset date"}},
+ )
+
+ def test_create_consultation_with_correct_cure_date(self):
+ data = self.consultation_data.copy()
+ data["create_symptoms"] = [
+ {
+ "symptom": Symptom.FEVER,
+ "onset_date": now() - timedelta(days=1),
+ "cure_date": now(),
+ },
+ ]
+ response = self.client.post(
+ "/api/v1/consultation/",
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(EncounterSymptom.objects.count(), 1)
+ self.assertEqual(
+ EncounterSymptom.objects.first().clinical_impression_status,
+ ClinicalImpressionStatus.COMPLETED,
+ )
+
+
+class TestEncounterSymptomApi(TestUtils, APITestCase):
+ @classmethod
+ def setUpTestData(cls) -> None:
+ cls.state = cls.create_state()
+ cls.district = cls.create_district(cls.state)
+ cls.local_body = cls.create_local_body(cls.district)
+ cls.super_user = cls.create_super_user("su", cls.district)
+ cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body)
+ cls.location = cls.create_asset_location(cls.facility)
+ cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility)
+ cls.doctor = cls.create_user(
+ "doctor", cls.district, home_facility=cls.facility, user_type=15
+ )
+ cls.patient = cls.create_patient(cls.district, cls.facility)
+ cls.consultation = cls.create_consultation(
+ cls.patient, cls.facility, cls.doctor
+ )
+
+ def get_url(self, symptom=None):
+ if symptom:
+ return f"/api/v1/consultation/{self.consultation.external_id}/symptoms/{symptom.external_id}/"
+ return f"/api/v1/consultation/{self.consultation.external_id}/symptoms/"
+
+ def test_create_new_symptom(self):
+ data = {
+ "symptom": Symptom.FEVER,
+ "onset_date": now(),
+ }
+ response = self.client.post(
+ self.get_url(),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(EncounterSymptom.objects.count(), 1)
+
+ def test_create_static_symptom_with_other_symptom(self):
+ data = {
+ "symptom": Symptom.FEVER,
+ "other_symptom": "Other Symptom",
+ "onset_date": now(),
+ }
+ response = self.client.post(
+ self.get_url(),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {
+ "other_symptom": [
+ "Other symptom should be empty when symptom type is not OTHERS"
+ ]
+ },
+ )
+
+ def test_create_others_symptom(self):
+ data = {
+ "symptom": Symptom.OTHERS,
+ "other_symptom": "Other Symptom",
+ "onset_date": now(),
+ }
+ response = self.client.post(
+ self.get_url(),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(EncounterSymptom.objects.count(), 1)
+
+ def test_create_other_symptom_without_other_symptom(self):
+ data = {
+ "symptom": Symptom.OTHERS,
+ "onset_date": now(),
+ }
+ response = self.client.post(
+ self.get_url(),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {
+ "other_symptom": [
+ "Other symptom should not be empty when symptom type is OTHERS"
+ ]
+ },
+ )
+
+ def test_create_duplicate_symptoms(self):
+ EncounterSymptom.objects.create(
+ consultation=self.consultation,
+ symptom=Symptom.FEVER,
+ onset_date=now(),
+ created_by=self.user,
+ )
+ data = {
+ "symptom": Symptom.FEVER,
+ "onset_date": now(),
+ }
+ response = self.client.post(
+ self.get_url(),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"symptom": ["An active symptom with the same details already exists"]},
+ )
+
+ def test_update_symptom(self):
+ symptom = EncounterSymptom.objects.create(
+ consultation=self.consultation,
+ symptom=Symptom.FEVER,
+ onset_date=now(),
+ created_by=self.user,
+ )
+ data = {
+ "cure_date": now(),
+ }
+ response = self.client.patch(
+ self.get_url(symptom),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(EncounterSymptom.objects.count(), 1)
+
+ def test_create_onset_date_in_future(self):
+ data = {
+ "symptom": Symptom.FEVER,
+ "onset_date": now() + timedelta(days=1),
+ }
+ response = self.client.post(
+ self.get_url(),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"onset_date": ["Onset date cannot be in the future"]},
+ )
+
+ def test_cure_date_before_onset_date(self):
+ symptom = EncounterSymptom.objects.create(
+ consultation=self.consultation,
+ symptom=Symptom.FEVER,
+ onset_date=now(),
+ created_by=self.user,
+ )
+ data = {
+ "cure_date": now() - timedelta(days=1),
+ }
+ response = self.client.patch(
+ self.get_url(symptom),
+ data,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json(),
+ {"cure_date": ["Cure date should be after onset date"]},
+ )
+
+ def test_mark_symptom_as_error(self):
+ symptom = EncounterSymptom.objects.create(
+ consultation=self.consultation,
+ symptom=Symptom.FEVER,
+ onset_date=now(),
+ created_by=self.user,
+ )
+ response = self.client.delete(
+ self.get_url(symptom),
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+ self.assertEqual(
+ EncounterSymptom.objects.get(id=symptom.id).clinical_impression_status,
+ ClinicalImpressionStatus.ENTERED_IN_ERROR,
+ )
diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py
index 5a4104b1ec..270cff9157 100644
--- a/care/facility/tests/test_patient_consultation_api.py
+++ b/care/facility/tests/test_patient_consultation_api.py
@@ -1,11 +1,12 @@
import datetime
from unittest.mock import patch
-from django.utils.timezone import make_aware
+from django.utils.timezone import make_aware, now
from rest_framework import status
from rest_framework.test import APITestCase
from care.facility.api.serializers.patient_consultation import MIN_ENCOUNTER_DATE
+from care.facility.models.encounter_symptom import Symptom
from care.facility.models.file_upload import FileUpload
from care.facility.models.icd11_diagnosis import (
ConditionVerificationStatus,
@@ -37,7 +38,6 @@ def setUpTestData(cls) -> None:
def get_default_data(self):
return {
"route_to_facility": 10,
- "symptoms": [1],
"category": CATEGORY_CHOICES[0][0],
"examination_details": "examination_details",
"history_of_present_illness": "history_of_present_illness",
@@ -51,6 +51,17 @@ def get_default_data(self):
"verification_status": ConditionVerificationStatus.CONFIRMED,
}
],
+ "create_symptoms": [
+ {
+ "symptom": Symptom.FEVER,
+ "onset_date": now(),
+ },
+ {
+ "symptom": Symptom.OTHERS,
+ "other_symptom": "Other Symptom",
+ "onset_date": now(),
+ },
+ ],
"patient_no": datetime.datetime.now().timestamp(),
}
@@ -419,9 +430,7 @@ def test_update_consultation_after_discharge(self):
)
self.assertEqual(res.status_code, status.HTTP_200_OK)
- res = self.update_consultation(
- consultation, symptoms=[1, 2], category="MILD", suggestion="A"
- )
+ res = self.update_consultation(consultation, category="MILD", suggestion="A")
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
def test_add_diagnoses_and_duplicate_diagnoses(self):
diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py
index ac3afd1665..acec25ea20 100644
--- a/care/facility/utils/reports/discharge_summary.py
+++ b/care/facility/utils/reports/discharge_summary.py
@@ -14,6 +14,7 @@
from care.facility.models import (
DailyRound,
Disease,
+ EncounterSymptom,
InvestigationValue,
PatientConsultation,
PatientSample,
@@ -21,6 +22,7 @@
PrescriptionDosageType,
PrescriptionType,
)
+from care.facility.models.encounter_symptom import ClinicalImpressionStatus
from care.facility.models.file_upload import FileUpload
from care.facility.models.icd11_diagnosis import (
ACTIVE_CONDITION_VERIFICATION_STATUSES,
@@ -97,6 +99,9 @@ def get_discharge_summary_data(consultation: PatientConsultation):
)
hcx = Policy.objects.filter(patient=consultation.patient)
daily_rounds = DailyRound.objects.filter(consultation=consultation)
+ symptoms = EncounterSymptom.objects.filter(consultation=consultation).exclude(
+ clinical_impression_status=ClinicalImpressionStatus.ENTERED_IN_ERROR
+ )
diagnoses = get_diagnoses_data(consultation)
investigations = InvestigationValue.objects.filter(
Q(consultation=consultation.id)
@@ -133,6 +138,7 @@ def get_discharge_summary_data(consultation: PatientConsultation):
"patient": consultation.patient,
"samples": samples,
"hcx": hcx,
+ "symptoms": symptoms,
"principal_diagnoses": diagnoses["principal"],
"unconfirmed_diagnoses": diagnoses["unconfirmed"],
"provisional_diagnoses": diagnoses["provisional"],
diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html
index 48c05155b1..06c7969b10 100644
--- a/care/templates/reports/patient_discharge_summary_pdf.html
+++ b/care/templates/reports/patient_discharge_summary_pdf.html
@@ -133,18 +133,6 @@
{{consultation.height}} cm
- {% if consultation.route_to_facility %}
-
-
- Symptoms at admission:
- {{consultation.get_symptoms_display|title}}
-
-
- From:
- {{consultation.symptoms_onset_date.date}}
-
-
- {% endif %}
{% if hcx %}
@@ -193,6 +181,49 @@
{% endif %}
+ {% if symptoms %}
+
+ Symptoms:
+
+
+
+
+
+
+ Name
+ |
+
+ Onset Date
+ |
+
+ Cure Date
+ |
+
+
+
+ {% for symptom in symptoms %}
+
+
+ {% if symptom.symptom == 9 %}
+ {{symptom.other_symptom}}
+ {% else %}
+ {{symptom.get_symptom_display}}
+ {% endif %}
+ |
+
+ {{symptom.onset_date.date}}
+ |
+
+ {{symptom.cure_date.date}}
+ |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
{% if principal_diagnosis %}
Principal Diagnosis (as per ICD-11 recommended by WHO):
@@ -765,14 +796,6 @@
{{daily_round.other_details}}
-
-
- Symptoms
-
-
- {{daily_round.additional_symptoms}}
-
-
diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py
index acb286f043..465fa74c87 100644
--- a/care/utils/tests/test_utils.py
+++ b/care/utils/tests/test_utils.py
@@ -12,7 +12,6 @@
from care.facility.models import (
CATEGORY_CHOICES,
DISEASE_CHOICES_MAP,
- SYMPTOM_CHOICES,
Ambulance,
Disease,
DiseaseStatusEnum,
@@ -307,13 +306,8 @@ def create_patient(cls, district: District, facility: Facility, **kwargs):
return patient
@classmethod
- def get_consultation_data(cls):
+ def get_consultation_data(cls) -> dict:
return {
- "patient": cls.patient,
- "facility": cls.facility,
- "symptoms": [SYMPTOM_CHOICES[0][0], SYMPTOM_CHOICES[1][0]],
- "other_symptoms": "No other symptoms",
- "symptoms_onset_date": make_aware(datetime(2020, 4, 7, 15, 30)),
"category": CATEGORY_CHOICES[0][0],
"examination_details": "examination_details",
"history_of_present_illness": "history_of_present_illness",
@@ -321,14 +315,12 @@ def get_consultation_data(cls):
"suggestion": PatientConsultation.SUGGESTION_CHOICES[0][
0
], # HOME ISOLATION
- "referred_to": None,
"encounter_date": make_aware(datetime(2020, 4, 7, 15, 30)),
"discharge_date": None,
"consultation_notes": "",
"course_in_facility": "",
- "created_date": mock_equal,
- "modified_date": mock_equal,
"patient_no": int(datetime.now().timestamp() * 1000),
+ "route_to_facility": 10,
}
@classmethod
@@ -336,6 +328,7 @@ def create_consultation(
cls,
patient: PatientRegistration,
facility: Facility,
+ doctor: User | None = None,
referred_to=None,
**kwargs,
) -> PatientConsultation:
@@ -345,6 +338,7 @@ def create_consultation(
"patient": patient,
"facility": facility,
"referred_to": referred_to,
+ "treating_physician": doctor,
}
)
data.update(kwargs)
diff --git a/config/api_router.py b/config/api_router.py
index 70512b97c0..ab8787fcc2 100644
--- a/config/api_router.py
+++ b/config/api_router.py
@@ -33,6 +33,7 @@
ConsultationDiagnosisViewSet,
)
from care.facility.api.viewsets.daily_round import DailyRoundsViewSet
+from care.facility.api.viewsets.encounter_symptom import EncounterSymptomViewSet
from care.facility.api.viewsets.events import (
EventTypeViewSet,
PatientConsultationEventViewSet,
@@ -227,6 +228,7 @@
)
consultation_nested_router.register(r"daily_rounds", DailyRoundsViewSet)
consultation_nested_router.register(r"diagnoses", ConsultationDiagnosisViewSet)
+consultation_nested_router.register(r"symptoms", EncounterSymptomViewSet)
consultation_nested_router.register(r"investigation", InvestigationValueViewSet)
consultation_nested_router.register(r"prescriptions", ConsultationPrescriptionViewSet)
consultation_nested_router.register(
diff --git a/data/dummy/facility.json b/data/dummy/facility.json
index 9976ca3646..7b7485df8c 100644
--- a/data/dummy/facility.json
+++ b/data/dummy/facility.json
@@ -1952,9 +1952,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "3",
- "other_symptoms": "",
- "symptoms_onset_date": "2022-09-27T07:19:53.380Z",
"deprecated_covid_category": null,
"category": "Moderate",
"examination_details": "",
@@ -2023,9 +2020,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2094,9 +2088,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2165,9 +2156,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2236,9 +2224,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2307,9 +2292,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2378,9 +2360,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2449,9 +2428,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2520,9 +2496,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2591,9 +2564,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2662,9 +2632,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2733,9 +2700,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2804,9 +2768,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2875,9 +2836,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -2946,9 +2904,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3017,9 +2972,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3088,9 +3040,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3159,9 +3108,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3230,9 +3176,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3301,9 +3244,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3372,9 +3312,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3443,9 +3380,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3514,9 +3448,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3585,9 +3516,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3656,9 +3584,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
@@ -3727,9 +3652,6 @@
"deprecated_icd11_provisional_diagnoses": "[]",
"deprecated_icd11_diagnoses": "[]",
"deprecated_icd11_principal_diagnosis": "",
- "symptoms": "1",
- "other_symptoms": "",
- "symptoms_onset_date": null,
"deprecated_covid_category": null,
"category": "Stable",
"examination_details": "Examination details and Clinical conditions",
From 9edfed086129f06b1e668e8e25d3d7d44b38be48 Mon Sep 17 00:00:00 2001
From: Rithvik Nishad
Date: Tue, 28 May 2024 16:10:39 +0530
Subject: [PATCH 3/8] Adds support to specify and filter ration card category
of a patient (#2201)
* Adds support to specify and filter ration card category of a patient
* Show ration card category in discharge summary
---------
Co-authored-by: Vignesh Hari
---
care/facility/api/viewsets/patient.py | 3 +-
...istration_ration_card_category_and_more.py | 38 +++++++++++++++++++
care/facility/models/patient.py | 11 +++++-
.../patient_discharge_summary_pdf.html | 3 ++
care/utils/tests/test_utils.py | 2 +
5 files changed, 55 insertions(+), 2 deletions(-)
create mode 100644 care/facility/migrations/0439_historicalpatientregistration_ration_card_category_and_more.py
diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py
index 36748513ec..f61b7ffb0e 100644
--- a/care/facility/api/viewsets/patient.py
+++ b/care/facility/api/viewsets/patient.py
@@ -73,7 +73,7 @@
ConditionVerificationStatus,
)
from care.facility.models.notification import Notification
-from care.facility.models.patient import PatientNotesEdit
+from care.facility.models.patient import PatientNotesEdit, RationCardCategory
from care.facility.models.patient_base import (
DISEASE_STATUS_DICT,
NewDischargeReasonEnum,
@@ -124,6 +124,7 @@ class PatientFilterSet(filters.FilterSet):
method="filter_by_category",
choices=CATEGORY_CHOICES,
)
+ ration_card_category = filters.ChoiceFilter(choices=RationCardCategory.choices)
def filter_by_category(self, queryset, name, value):
if value:
diff --git a/care/facility/migrations/0439_historicalpatientregistration_ration_card_category_and_more.py b/care/facility/migrations/0439_historicalpatientregistration_ration_card_category_and_more.py
new file mode 100644
index 0000000000..e40f20b2cc
--- /dev/null
+++ b/care/facility/migrations/0439_historicalpatientregistration_ration_card_category_and_more.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.2.8 on 2024-05-28 05:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("facility", "0438_alter_dailyround_patient_category_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="historicalpatientregistration",
+ name="ration_card_category",
+ field=models.CharField(
+ choices=[
+ ("NO_CARD", "Non-card holder"),
+ ("BPL", "BPL"),
+ ("APL", "APL"),
+ ],
+ max_length=8,
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="patientregistration",
+ name="ration_card_category",
+ field=models.CharField(
+ choices=[
+ ("NO_CARD", "Non-card holder"),
+ ("BPL", "BPL"),
+ ("APL", "APL"),
+ ],
+ max_length=8,
+ null=True,
+ ),
+ ),
+ ]
diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py
index 94a06f0f5f..9c30e76914 100644
--- a/care/facility/models/patient.py
+++ b/care/facility/models/patient.py
@@ -7,6 +7,7 @@
from django.db.models import Case, F, Func, JSONField, Value, When
from django.db.models.functions import Coalesce, Now
from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords
from care.abdm.models import AbhaNumber
@@ -43,6 +44,12 @@
from care.utils.models.validators import mobile_or_landline_number_validator
+class RationCardCategory(models.TextChoices):
+ NON_CARD_HOLDER = "NO_CARD", _("Non-card holder")
+ BPL = "BPL", _("BPL")
+ APL = "APL", _("APL")
+
+
class PatientRegistration(PatientBaseModel, PatientPermissionMixin):
# fields in the PatientSearch model
PATIENT_SEARCH_KEYS = [
@@ -140,7 +147,9 @@ class TestTypeEnum(enum.Enum):
default="",
verbose_name="Passport Number of Foreign Patients",
)
- # aadhar_no = models.CharField(max_length=255, default="", verbose_name="Aadhar Number of Patient")
+ ration_card_category = models.CharField(
+ choices=RationCardCategory.choices, null=True, max_length=8
+ )
is_medical_worker = models.BooleanField(
default=False, verbose_name="Is the Patient a Medical Worker"
diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html
index 06c7969b10..b5b1594b64 100644
--- a/care/templates/reports/patient_discharge_summary_pdf.html
+++ b/care/templates/reports/patient_discharge_summary_pdf.html
@@ -66,6 +66,9 @@
Address: {{patient.address}}
+
+ Ration Card Category: {{patient.get_ration_card_category_display|field_name_to_label}}
+
diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py
index 465fa74c87..20fbfee7d2 100644
--- a/care/utils/tests/test_utils.py
+++ b/care/utils/tests/test_utils.py
@@ -31,6 +31,7 @@
ConsultationDiagnosis,
ICD11Diagnosis,
)
+from care.facility.models.patient import RationCardCategory
from care.users.models import District, State
@@ -275,6 +276,7 @@ def get_patient_data(cls, district, state) -> dict:
"date_of_receipt_of_information": make_aware(
datetime(2020, 4, 1, 15, 30, 00)
),
+ "ration_card_category": RationCardCategory.NON_CARD_HOLDER,
}
@classmethod
From 0e947b8142978860a3d350c2549e360aa297cb36 Mon Sep 17 00:00:00 2001
From: Aakash Singh
Date: Tue, 28 May 2024 16:11:37 +0530
Subject: [PATCH 4/8] fix redis tokenization for icd11 ids (#2193)
* fix redis tokenization for icd11 ids
* remove index on the default label field
* rename search vector field to be consistent with other models
* add test case
---------
Co-authored-by: Vignesh Hari
---
Pipfile | 2 +-
Pipfile.lock | 110 +++++++++++++-------------
care/facility/api/viewsets/icd.py | 2 +-
care/facility/static_data/icd11.py | 5 +-
care/facility/tests/test_icd11_api.py | 3 +
care/utils/static_data/helpers.py | 4 +-
6 files changed, 66 insertions(+), 60 deletions(-)
diff --git a/Pipfile b/Pipfile
index 2d91f60a26..df4012ccbe 100644
--- a/Pipfile
+++ b/Pipfile
@@ -41,10 +41,10 @@ pyjwt = "==2.8.0"
python-slugify = "==8.0.1"
pywebpush = "==1.14.0"
redis = {extras = ["hiredis"], version = "<5.0.0"} # constraint for redis-om
+redis-om = "==0.3.1"
requests = "==2.31.0"
sentry-sdk = "==1.30.0"
whitenoise = "==6.6.0"
-redis-om = "==0.2.1"
[dev-packages]
black = "==24.4.2"
diff --git a/Pipfile.lock b/Pipfile.lock
index 1007041312..5efbc2e6ac 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "3225dfd3c9b038bca4b17ef7ea788a08cbaf926bf1e12d78c4d6add45a675816"
+ "sha256": "bdb4245bd4ae7a35663200ca87b7da1815e7d62b411e04c021df1a02361244c8"
},
"pipfile-spec": 6,
"requires": {
@@ -104,11 +104,11 @@
},
"botocore": {
"hashes": [
- "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431",
- "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939"
+ "sha256:449912ba3c4ded64f21d09d428146dd9c05337b2a112e15511bf2c4888faae79",
+ "sha256:8ca87776450ef41dd25c327eb6e504294230a5756940d68bcfdedc4a7cdeca97"
],
"markers": "python_version >= '3.8'",
- "version": "==1.34.103"
+ "version": "==1.34.113"
},
"celery": {
"hashes": [
@@ -733,11 +733,11 @@
},
"more-itertools": {
"hashes": [
- "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d",
- "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"
+ "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684",
+ "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"
],
- "markers": "python_version >= '3.7'",
- "version": "==9.1.0"
+ "markers": "python_version >= '3.8'",
+ "version": "==10.2.0"
},
"newrelic": {
"hashes": [
@@ -982,7 +982,7 @@
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.9.0.post0"
},
"python-fsutil": {
@@ -1093,12 +1093,12 @@
},
"redis-om": {
"hashes": [
- "sha256:150c9cb5238d6003f35e9b6394aab30a0df35b00e955eb7dc508f4345e0a0ccc",
- "sha256:31313a3027a014608b3a4d44ecd1d3000c7d0fe3a25060db19b42225e636cd53"
+ "sha256:1a1eea45a507da3541a6afa982c7aecae2d58920c756525198917afc433504ee",
+ "sha256:c521b4e60d7bbdf537642f5b94d004330a095dcc1e4daf6efec8e46b0a2f2799"
],
"index": "pypi",
- "markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==0.2.1"
+ "markers": "python_version >= '3.8' and python_version < '4.0'",
+ "version": "==0.3.1"
},
"referencing": {
"hashes": [
@@ -1243,7 +1243,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlparse": {
@@ -1287,19 +1287,19 @@
},
"types-setuptools": {
"hashes": [
- "sha256:a4381e041510755a6c9210e26ad55b1629bc10237aeb9cb8b6bd24996b73db48",
- "sha256:a7ba908f1746c4337d13f027fa0f4a5bcad6d1d92048219ba792b3295c58586d"
+ "sha256:8f5379b9948682d72a9ab531fbe52932e84c4f38deda570255f9bae3edd766bc",
+ "sha256:e31fee7b9d15ef53980526579ac6089b3ae51a005a281acf97178e90ac71aff6"
],
"markers": "python_version >= '3.8'",
- "version": "==69.5.0.20240423"
+ "version": "==70.0.0.20240524"
},
"typing-extensions": {
"hashes": [
- "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
- "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
+ "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8",
+ "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"
],
"markers": "python_version >= '3.8'",
- "version": "==4.11.0"
+ "version": "==4.12.0"
},
"tzdata": {
"hashes": [
@@ -1328,7 +1328,7 @@
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
],
- "markers": "python_version >= '3.8'",
+ "markers": "python_version >= '3.6'",
"version": "==2.2.1"
},
"vine": {
@@ -1374,11 +1374,11 @@
},
"autopep8": {
"hashes": [
- "sha256:1fa8964e4618929488f4ec36795c7ff12924a68b8bf01366c094fc52f770b6e7",
- "sha256:2bb76888c5edbcafe6aabab3c47ba534f5a2c2d245c2eddced4a30c4b4946357"
+ "sha256:57c1026ee3ee40f57c5b93073b705f8e30aa52411fca33306d730274d2882bba",
+ "sha256:bc9b267f14d358a9af574b95e95a661681c60a275ffce419ba5fb4eae9920bcc"
],
"markers": "python_version >= '3.8'",
- "version": "==2.1.0"
+ "version": "==2.1.1"
},
"black": {
"hashes": [
@@ -1432,11 +1432,11 @@
},
"botocore": {
"hashes": [
- "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431",
- "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939"
+ "sha256:449912ba3c4ded64f21d09d428146dd9c05337b2a112e15511bf2c4888faae79",
+ "sha256:8ca87776450ef41dd25c327eb6e504294230a5756940d68bcfdedc4a7cdeca97"
],
"markers": "python_version >= '3.8'",
- "version": "==1.34.103"
+ "version": "==1.34.113"
},
"botocore-stubs": {
"hashes": [
@@ -1758,11 +1758,11 @@
},
"faker": {
"hashes": [
- "sha256:2107618cf306bb188dcfea3e5cfd94aa92d65c7293a2437c1e96a99c83274755",
- "sha256:24e28dce0b89683bb9e017e042b971c8c4909cff551b6d46f1e207674c7c2526"
+ "sha256:45b84f47ff1ef86e3d1a8d11583ca871ecf6730fad0660edadc02576583a2423",
+ "sha256:cfe97c4857c4c36ee32ea4aaabef884895992e209bae4cbd26807cf3e05c6918"
],
"markers": "python_version >= '3.8'",
- "version": "==25.1.0"
+ "version": "==25.2.0"
},
"filelock": {
"hashes": [
@@ -1966,10 +1966,10 @@
},
"mypy-boto3-s3": {
"hashes": [
- "sha256:0d37161fd0cd7ebf194cf9ccadb9101bf5c9b2426c2d00677b7e644d6f2298e4",
- "sha256:70c8bad00db70704fb7ac0ee1440c7eb0587578ae9a2b00997f29f17f60f45e7"
+ "sha256:95fbc6bcba2bb03c20a97cc5cf60ff66c6842c8c4fc4183c49bfa35905d5a1ee",
+ "sha256:a137bca9bbe86c0fe35bbf36a2d44ab62526f41bb683550dd6cfbb5a10ede832"
],
- "version": "==1.34.91"
+ "version": "==1.34.105"
},
"mypy-extensions": {
"hashes": [
@@ -2021,11 +2021,11 @@
},
"platformdirs": {
"hashes": [
- "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf",
- "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"
+ "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee",
+ "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"
],
"markers": "python_version >= '3.8'",
- "version": "==4.2.1"
+ "version": "==4.2.2"
},
"pre-commit": {
"hashes": [
@@ -2087,7 +2087,7 @@
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.9.0.post0"
},
"pyyaml": {
@@ -2175,18 +2175,18 @@
},
"setuptools": {
"hashes": [
- "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987",
- "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"
+ "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4",
+ "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"
],
"markers": "python_version >= '3.8'",
- "version": "==69.5.1"
+ "version": "==70.0.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlparse": {
@@ -2247,11 +2247,11 @@
},
"types-requests": {
"hashes": [
- "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1",
- "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"
+ "sha256:26b8a6de32d9f561192b9942b41c0ab2d8010df5677ca8aa146289d11d505f57",
+ "sha256:f19ed0e2daa74302069bbbbf9e82902854ffa780bc790742a810a9aaa52f65ec"
],
"markers": "python_version >= '3.8'",
- "version": "==2.31.0.20240406"
+ "version": "==2.32.0.20240523"
},
"types-s3transfer": {
"hashes": [
@@ -2263,27 +2263,27 @@
},
"typing-extensions": {
"hashes": [
- "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
- "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
+ "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8",
+ "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"
],
"markers": "python_version >= '3.8'",
- "version": "==4.11.0"
+ "version": "==4.12.0"
},
"urllib3": {
"hashes": [
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
],
- "markers": "python_version >= '3.8'",
+ "markers": "python_version >= '3.6'",
"version": "==2.2.1"
},
"virtualenv": {
"hashes": [
- "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b",
- "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"
+ "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c",
+ "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"
],
"markers": "python_version >= '3.7'",
- "version": "==20.26.1"
+ "version": "==20.26.2"
},
"watchdog": {
"hashes": [
@@ -2582,11 +2582,11 @@
},
"mdit-py-plugins": {
"hashes": [
- "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9",
- "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"
+ "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a",
+ "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"
],
"markers": "python_version >= '3.8'",
- "version": "==0.4.0"
+ "version": "==0.4.1"
},
"mdurl": {
"hashes": [
@@ -2772,7 +2772,7 @@
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
],
- "markers": "python_version >= '3.8'",
+ "markers": "python_version >= '3.6'",
"version": "==2.2.1"
}
}
diff --git a/care/facility/api/viewsets/icd.py b/care/facility/api/viewsets/icd.py
index 886348c8cf..0b2bcc5a86 100644
--- a/care/facility/api/viewsets/icd.py
+++ b/care/facility/api/viewsets/icd.py
@@ -28,7 +28,7 @@ def list(self, request):
query = [ICD11.has_code == 1]
if q := request.query_params.get("query"):
- query.append(ICD11.label % query_builder(q))
+ query.append(ICD11.vec % query_builder(q))
result = FindQuery(expressions=query, model=ICD11, limit=limit).execute(
exhaust_results=False
diff --git a/care/facility/static_data/icd11.py b/care/facility/static_data/icd11.py
index f33b2e6371..dd379671e5 100644
--- a/care/facility/static_data/icd11.py
+++ b/care/facility/static_data/icd11.py
@@ -19,10 +19,12 @@ class ICD11Object(TypedDict):
class ICD11(BaseRedisModel):
id: int = Field(primary_key=True)
- label: str = Field(index=True, full_text_search=True)
+ label: str
chapter: str
has_code: int = Field(index=True)
+ vec: str = Field(index=True, full_text_search=True)
+
def get_representation(self) -> ICD11Object:
return {
"id": self.id,
@@ -45,6 +47,7 @@ def load_icd11_diagnosis():
label=diagnosis[1],
chapter=diagnosis[2] or "",
has_code=1 if re.match(DISEASE_CODE_PATTERN, diagnosis[1]) else 0,
+ vec=diagnosis[1].replace(".", "\\.", 1),
).save()
Migrator().run()
print("Done")
diff --git a/care/facility/tests/test_icd11_api.py b/care/facility/tests/test_icd11_api.py
index 69bacc7029..f18f2a9c75 100644
--- a/care/facility/tests/test_icd11_api.py
+++ b/care/facility/tests/test_icd11_api.py
@@ -39,6 +39,9 @@ def test_search_with_disease_code(self):
res = self.search_icd11("ME24.A1")
self.assertContains(res, "ME24.A1 Haemorrhage of anus and rectum")
+ res = self.search_icd11("CA22.Z")
+ self.assertContains(res, "CA22.Z Chronic obstructive pulmonary disease")
+
res = self.search_icd11("1A00 Cholera")
self.assertContains(res, "1A00 Cholera")
diff --git a/care/utils/static_data/helpers.py b/care/utils/static_data/helpers.py
index 6c0f1c2567..ff0c611cc2 100644
--- a/care/utils/static_data/helpers.py
+++ b/care/utils/static_data/helpers.py
@@ -2,12 +2,12 @@
from redis_om.model.token_escaper import TokenEscaper
-token_escaper = TokenEscaper(re.compile(r"[,<>{}\[\]\\\"\':;!@#$%^&*()\-+=~\/ ]"))
+token_escaper = TokenEscaper(re.compile(r"[,.<>{}\[\]\\\"\':;!@#$%^&*()\-+=~\/ ]"))
def query_builder(query: str) -> str:
"""
Builds a query for redis full text search from a given query string.
"""
- words = query.strip().rstrip(".").rsplit(maxsplit=3)
+ words = query.strip().rsplit(maxsplit=3)
return f"{'* '.join([token_escaper.escape(word) for word in words])}*"
From 28bedb1c07b48ecc11d5e4f177532e7567ad24f3 Mon Sep 17 00:00:00 2001
From: Rithvik Nishad
Date: Tue, 28 May 2024 16:12:09 +0530
Subject: [PATCH 5/8] Fixes state and district admin not able to see users of
same user type level (#2200)
* Fixes state and district admin not able to see users of same user type level
* correct test
---------
Co-authored-by: Vignesh Hari
---
care/users/api/viewsets/users.py | 4 +--
care/users/tests/test_facility_user_create.py | 32 ++++++++++++++-----
2 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py
index 7916d08418..f2152d3762 100644
--- a/care/users/api/viewsets/users.py
+++ b/care/users/api/viewsets/users.py
@@ -127,7 +127,7 @@ def get_queryset(self):
if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]:
query |= Q(
state=self.request.user.state,
- user_type__lt=User.TYPE_VALUE_MAP["StateAdmin"],
+ user_type__lte=User.TYPE_VALUE_MAP["StateAdmin"],
is_superuser=False,
)
elif (
@@ -135,7 +135,7 @@ def get_queryset(self):
):
query |= Q(
district=self.request.user.district,
- user_type__lt=User.TYPE_VALUE_MAP["DistrictAdmin"],
+ user_type__lte=User.TYPE_VALUE_MAP["DistrictAdmin"],
is_superuser=False,
)
else:
diff --git a/care/users/tests/test_facility_user_create.py b/care/users/tests/test_facility_user_create.py
index 54d7edea3b..e8af56e9cd 100644
--- a/care/users/tests/test_facility_user_create.py
+++ b/care/users/tests/test_facility_user_create.py
@@ -16,6 +16,12 @@ def setUpTestData(cls) -> None:
cls.super_user = cls.create_super_user("su", cls.district)
cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body)
cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility)
+ cls.state_admin = cls.create_user(
+ "stateadmin1",
+ cls.district,
+ home_facility=cls.facility,
+ user_type=User.TYPE_VALUE_MAP["StateAdmin"],
+ )
def get_base_url(self):
return "/api/v1/users/add_user/"
@@ -46,8 +52,8 @@ def get_detail_representation(self, obj: User = None) -> dict:
"ward": getattr(obj.ward, "id", None),
}
- def get_new_user_data(self):
- return {
+ def get_user_data(self, **kwargs):
+ data = {
"username": "roopak",
"user_type": "Staff",
"phone_number": "+917795937091",
@@ -60,18 +66,28 @@ def get_new_user_data(self):
"verified": True,
"facilities": [self.facility.external_id],
}
+ data.update(kwargs)
+ return data.copy()
def test_create_facility_user__should_fail__when_higher_level(self):
- data = self.get_new_user_data().copy()
- data.update({"user_type": "DistrictAdmin"})
-
+ data = self.get_user_data(user_type="DistrictAdmin")
response = self.client.post(self.get_base_url(), data=data, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_facility_user__should_fail__when_different_location(self):
new_district = self.clone_object(self.district)
- data = self.get_new_user_data().copy()
- data.update({"district": new_district.id})
-
+ data = self.get_user_data(district=new_district.id)
response = self.client.post(self.get_base_url(), data=data, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_user_of_same_type(self):
+ self.client.force_authenticate(self.state_admin)
+
+ data = self.get_user_data(
+ username="stateadmin2", user_type=User.TYPE_VALUE_MAP["StateAdmin"]
+ )
+ res = self.client.post(self.get_base_url(), data=data, format="json")
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+
+ res = self.client.get("/api/v1/users/", {"username": "stateadmin2"})
+ self.assertContains(res, "stateadmin2")
From 5c88ca146088f75e028b563172ab38cfb65cc654 Mon Sep 17 00:00:00 2001
From: Aakash Singh
Date: Tue, 28 May 2024 16:14:53 +0530
Subject: [PATCH 6/8] merge migrations (#2202)
---
.../migrations/0440_merge_20240528_1613.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
create mode 100644 care/facility/migrations/0440_merge_20240528_1613.py
diff --git a/care/facility/migrations/0440_merge_20240528_1613.py b/care/facility/migrations/0440_merge_20240528_1613.py
new file mode 100644
index 0000000000..9fd0cd9bd3
--- /dev/null
+++ b/care/facility/migrations/0440_merge_20240528_1613.py
@@ -0,0 +1,15 @@
+# Generated by Django 4.2.10 on 2024-05-28 10:43
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("facility", "0439_encounter_symptoms"),
+ (
+ "facility",
+ "0439_historicalpatientregistration_ration_card_category_and_more",
+ ),
+ ]
+
+ operations = []
From 51235a952ee5b0fb1aee4c353c93050f11946ec0 Mon Sep 17 00:00:00 2001
From: Aakash Singh
Date: Tue, 28 May 2024 21:29:57 +0530
Subject: [PATCH 7/8] fix state admin access to daily rounds (#2203)
---
care/facility/models/daily_round.py | 3 +--
.../tests/test_patient_daily_rounds_api.py | 24 +++++++++++++++++++
2 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py
index 4b2457e50c..30c18b91c4 100644
--- a/care/facility/models/daily_round.py
+++ b/care/facility/models/daily_round.py
@@ -589,8 +589,7 @@ def has_object_read_permission(self, request):
request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]
and (
self.consultation.patient.facility
- and request.user.state
- == self.consultation.patient.facility.district
+ and request.user.state == self.consultation.patient.facility.state
)
)
)
diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py
index 06195fecb0..5145e4827e 100644
--- a/care/facility/tests/test_patient_daily_rounds_api.py
+++ b/care/facility/tests/test_patient_daily_rounds_api.py
@@ -16,6 +16,12 @@ def setUpTestData(cls) -> None:
cls.local_body = cls.create_local_body(cls.district)
cls.super_user = cls.create_super_user("su", cls.district)
cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body)
+ cls.state_admin = cls.create_user(
+ "state_admin", cls.district, home_facility=cls.facility, user_type=40
+ )
+ cls.district_admin = cls.create_user(
+ "district_admin", cls.district, home_facility=cls.facility, user_type=30
+ )
cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility)
cls.patient = cls.create_patient(district=cls.district, facility=cls.facility)
cls.asset_location = cls.create_asset_location(cls.facility)
@@ -72,6 +78,24 @@ def test_action_in_log_update(
patient.action, PatientRegistration.ActionEnum.DISCHARGE_RECOMMENDED.value
)
+ def test_log_update_access_by_state_admin(self):
+ self.client.force_authenticate(user=self.state_admin)
+ response = self.client.post(
+ f"/api/v1/consultation/{self.consultation_with_bed.external_id}/daily_rounds/",
+ data=self.log_update,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ def test_log_update_access_by_district_admin(self):
+ self.client.force_authenticate(user=self.district_admin)
+ response = self.client.post(
+ f"/api/v1/consultation/{self.consultation_with_bed.external_id}/daily_rounds/",
+ data=self.log_update,
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
def test_log_update_without_bed_for_admission(
self,
):
From 59c05b95d098b6718531e01fd3f745e784c28d66 Mon Sep 17 00:00:00 2001
From: Aakash Singh
Date: Sat, 1 Jun 2024 17:25:24 +0530
Subject: [PATCH 8/8] Fix discharge summary filters (#2210)
---
care/facility/templatetags/filters.py | 3 ++-
care/templates/reports/patient_discharge_summary_pdf.html | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/care/facility/templatetags/filters.py b/care/facility/templatetags/filters.py
index 9a8bc576fa..045819279c 100644
--- a/care/facility/templatetags/filters.py
+++ b/care/facility/templatetags/filters.py
@@ -22,7 +22,8 @@ def suggestion_string(suggestion_code: str):
@register.filter()
def field_name_to_label(value):
- return value.replace("_", " ").capitalize()
+ if value:
+ return value.replace("_", " ").capitalize()
@register.filter(expects_localtime=True)
diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html
index b5b1594b64..2a2125c788 100644
--- a/care/templates/reports/patient_discharge_summary_pdf.html
+++ b/care/templates/reports/patient_discharge_summary_pdf.html
@@ -67,7 +67,7 @@
Address: {{patient.address}}
- Ration Card Category: {{patient.get_ration_card_category_display|field_name_to_label}}
+ Ration Card Category: {{patient.get_ration_card_category_display}}