diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index a316153caa..f34acaced6 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -64,6 +64,7 @@ UserBaseMinimumSerializer, ) from care.users.models import User +from care.utils.lock import Lock from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_home_facility_queryset from care.utils.serializer.external_id_field import ExternalIdSerializerField @@ -191,6 +192,9 @@ def get_discharge_prn_prescription(self, consultation): dosage_type=PrescriptionDosageType.PRN.value, ).values() + def _lock_key(self, patient_id): + return f"patient_consultation__patient_registration__{patient_id}" + class Meta: model = PatientConsultation read_only_fields = TIMESTAMP_FIELDS + ( @@ -353,18 +357,16 @@ def create(self, validated_data): create_diagnosis = validated_data.pop("create_diagnoses") create_symptoms = validated_data.pop("create_symptoms") - action = -1 - review_interval = -1 - if "action" in validated_data: - action = validated_data.pop("action") - if "review_interval" in validated_data: - review_interval = validated_data.pop("review_interval") + + action = validated_data.pop("action", -1) + review_interval = validated_data.get("review_interval", -1) # Authorisation Check - allowed_facilities = get_home_facility_queryset(self.context["request"].user) + user = self.context["request"].user + allowed_facilities = get_home_facility_queryset(user) if not allowed_facilities.filter( - id=self.validated_data["patient"].facility.id + id=self.validated_data["patient"].facility_id ).exists(): raise ValidationError( {"facility": "Consultation creates are only allowed in home facility"} @@ -372,130 +374,127 @@ def create(self, validated_data): # End Authorisation Checks - if validated_data["patient"].last_consultation: + with Lock( + self._lock_key(validated_data["patient"].id), 30 + ), transaction.atomic(): + patient = validated_data["patient"] + if patient.last_consultation: + if patient.last_consultation.assigned_to == user: + raise ValidationError( + { + "Permission Denied": "Only Facility Staff can create consultation for a Patient" + }, + ) + + if not patient.last_consultation.discharge_date: + raise ValidationError( + {"consultation": "Exists please Edit Existing Consultation"} + ) + + if "is_kasp" in validated_data: + if validated_data["is_kasp"]: + validated_data["kasp_enabled_date"] = now() + + # Coercing facility as the patient's facility + validated_data["facility_id"] = patient.facility_id + + consultation: PatientConsultation = super().create(validated_data) + consultation.created_by = user + consultation.last_edited_by = user + consultation.previous_consultation = patient.last_consultation + last_consultation = patient.last_consultation if ( - self.context["request"].user - == validated_data["patient"].last_consultation.assigned_to + last_consultation + and consultation.suggestion == SuggestionChoices.A + and last_consultation.suggestion == SuggestionChoices.A + and last_consultation.discharge_date + and last_consultation.discharge_date + timedelta(days=30) + > consultation.encounter_date ): - raise ValidationError( - { - "Permission Denied": "Only Facility Staff can create consultation for a Patient" - }, - ) + consultation.is_readmission = True + + diagnosis = ConsultationDiagnosis.objects.bulk_create( + [ + ConsultationDiagnosis( + consultation=consultation, + diagnosis_id=obj["diagnosis"].id, + is_principal=obj["is_principal"], + verification_status=obj["verification_status"], + created_by=user, + ) + for obj in create_diagnosis + ] + ) - if validated_data["patient"].last_consultation: - if not validated_data["patient"].last_consultation.discharge_date: - raise ValidationError( - {"consultation": "Exists please Edit Existing Consultation"} + 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=user, ) + for obj in create_symptoms + ) - if "is_kasp" in validated_data: - if validated_data["is_kasp"]: - validated_data["kasp_enabled_date"] = localtime(now()) - - bed = validated_data.pop("bed", None) - - validated_data["facility_id"] = validated_data[ - "patient" - ].facility_id # Coercing facility as the patient's facility - consultation = super().create(validated_data) - consultation.created_by = self.context["request"].user - consultation.last_edited_by = self.context["request"].user - patient = consultation.patient - consultation.previous_consultation = patient.last_consultation - last_consultation = patient.last_consultation - if ( - last_consultation - and consultation.suggestion == SuggestionChoices.A - and last_consultation.suggestion == SuggestionChoices.A - and last_consultation.discharge_date - and last_consultation.discharge_date + timedelta(days=30) - > consultation.encounter_date - ): - consultation.is_readmission = True - consultation.save() - - diagnosis = ConsultationDiagnosis.objects.bulk_create( - [ - ConsultationDiagnosis( + bed = validated_data.pop("bed", None) + if bed and consultation.suggestion == SuggestionChoices.A: + consultation_bed = ConsultationBed( + bed=bed, consultation=consultation, - diagnosis_id=obj["diagnosis"].id, - is_principal=obj["is_principal"], - verification_status=obj["verification_status"], - created_by=self.context["request"].user, + start_date=consultation.created_date, ) - for obj in create_diagnosis - ] - ) + consultation_bed.save() + consultation.current_bed = consultation_bed - 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 consultation.suggestion == SuggestionChoices.OP: + consultation.discharge_date = now() + patient.is_active = False + patient.allow_transfer = True + else: + patient.is_active = True + patient.last_consultation = consultation - if bed and consultation.suggestion == SuggestionChoices.A: - consultation_bed = ConsultationBed( - bed=bed, - consultation=consultation, - start_date=consultation.created_date, - ) - consultation_bed.save() - consultation.current_bed = consultation_bed - consultation.save(update_fields=["current_bed"]) + if action != -1: + patient.action = action - if consultation.suggestion == SuggestionChoices.OP: - consultation.discharge_date = localtime(now()) - consultation.save() - patient.is_active = False - patient.allow_transfer = True - else: - patient.is_active = True - patient.last_consultation = consultation - - if action != -1: - patient.action = action - consultation.review_interval = review_interval - if review_interval > 0: - patient.review_time = localtime(now()) + timedelta(minutes=review_interval) - else: - patient.review_time = None + if review_interval > 0: + patient.review_time = now() + timedelta(minutes=review_interval) + else: + patient.review_time = None - patient.save() - NotificationGenerator( - event=Notification.Event.PATIENT_CONSULTATION_CREATED, - caused_by=self.context["request"].user, - caused_object=consultation, - facility=patient.facility, - ).generate() + consultation.save() + patient.save() - create_consultation_events( - consultation.id, - (consultation, *diagnosis, *symptoms), - consultation.created_by.id, - consultation.created_date, - ) + create_consultation_events( + consultation.id, + (consultation, *diagnosis, *symptoms), + consultation.created_by.id, + consultation.created_date, + ) - if consultation.assigned_to: NotificationGenerator( - event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, - caused_by=self.context["request"].user, + event=Notification.Event.PATIENT_CONSULTATION_CREATED, + caused_by=user, caused_object=consultation, - facility=consultation.patient.facility, - notification_mediums=[ - Notification.Medium.SYSTEM, - Notification.Medium.WHATSAPP, - ], + facility=patient.facility, ).generate() - return consultation + if consultation.assigned_to: + NotificationGenerator( + event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, + caused_by=user, + caused_object=consultation, + facility=consultation.patient.facility, + notification_mediums=[ + Notification.Medium.SYSTEM, + Notification.Medium.WHATSAPP, + ], + ).generate() + + return consultation def validate_create_diagnoses(self, value): # Reject if create_diagnoses is present for edits diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index 4fc1b857b2..95aa138331 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -1,3 +1,4 @@ +from django.db import transaction from django.db.models import Prefetch from django.db.models.query_utils import Q from django.shortcuts import get_object_or_404, render @@ -109,6 +110,10 @@ def get_queryset(self): applied_filters |= Q(facility=self.request.user.home_facility) return self.queryset.filter(applied_filters) + @transaction.non_atomic_requests + def create(self, request, *args, **kwargs) -> Response: + return super().create(request, *args, **kwargs) + @extend_schema(tags=["consultation"]) @action(detail=True, methods=["POST"]) def discharge_patient(self, request, *args, **kwargs): diff --git a/care/utils/lock.py b/care/utils/lock.py new file mode 100644 index 0000000000..7600105b38 --- /dev/null +++ b/care/utils/lock.py @@ -0,0 +1,35 @@ +from django.core.cache import cache +from rest_framework.exceptions import APIException + + +class ObjectLocked(APIException): + status_code = 423 + default_detail = "The resource you are trying to access is locked" + default_code = "object_locked" + + +class Lock: + def __init__(self, key, timeout=None): + self.key = f"lock:{key}" + self.timeout = timeout + + def acquire(self): + try: + if not cache.set(self.key, True, self.timeout, nx=True): + raise ObjectLocked() + # handle nx not supported + except TypeError: + if cache.get(self.key): + raise ObjectLocked() + cache.set(self.key, True, self.timeout) + + def release(self): + return cache.delete(self.key) + + def __enter__(self): + self.acquire() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.release() + return False