From e3786379f8d5de90f56a2c17f02859e56192d32e Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Sat, 18 Mar 2023 10:44:03 +0530 Subject: [PATCH 01/31] feat(patient notes): add edit window validation and update endpoint - Add edit window validation to restrict updates to notes after a certain time - Add PUT and PATCH endpoints to update notes --- care/facility/api/serializers/patient.py | 36 +++++-- care/facility/api/viewsets/patient.py | 35 +++++- .../models/mixins/permissions/patient.py | 100 ++++++++++++++---- 3 files changed, 141 insertions(+), 30 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 5a0bdbd7c5..8c0cc46550 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -132,7 +132,9 @@ class Meta: phone_number = PhoneNumberIsPossibleField() - facility = ExternalIdSerializerField(queryset=Facility.objects.all(), required=False) + facility = ExternalIdSerializerField( + queryset=Facility.objects.all(), required=False + ) medical_history = serializers.ListSerializer( child=MedicalHistorySerializer(), required=False ) @@ -386,11 +388,11 @@ class PatientSearchSerializer(serializers.ModelSerializer): class Meta: model = PatientSearch exclude = ( - "date_of_birth", - "year_of_birth", - "external_id", - "id", - ) + TIMESTAMP_FIELDS + "date_of_birth", + "year_of_birth", + "external_id", + "id", + ) + TIMESTAMP_FIELDS class PatientTransferSerializer(serializers.ModelSerializer): @@ -421,8 +423,18 @@ def save(self, **kwargs): class PatientNotesSerializer(serializers.ModelSerializer): + NOTE_EDIT_WINDOW = 30 * 60 # 30 minutes + facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) + edit_window = serializers.SerializerMethodField(read_only=True) + + def get_edit_window(self, obj): + return self.NOTE_EDIT_WINDOW + + def validate(self, data): + data["edit_window"] = self.get_edit_window(data) + return data def validate_empty_values(self, data): if not data.get("note", "").strip(): @@ -431,5 +443,13 @@ def validate_empty_values(self, data): class Meta: model = PatientNotes - fields = ("note", "facility", "created_by_object", "created_date") - read_only_fields = ("created_date",) + fields = ( + "id", + "note", + "facility", + "created_by_object", + "created_date", + "modified_date", + "edit_window", + ) + read_only_fields = ("id", "created_date", "modified_date") diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index fbb96cee17..22efdb8109 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -15,7 +15,12 @@ from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.generics import get_object_or_404 -from rest_framework.mixins import CreateModelMixin, ListModelMixin, RetrieveModelMixin +from rest_framework.mixins import ( + CreateModelMixin, + ListModelMixin, + RetrieveModelMixin, + UpdateModelMixin, +) from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -604,13 +609,18 @@ def list(self, request, *args, **kwargs): class PatientNotesViewSet( - ListModelMixin, RetrieveModelMixin, CreateModelMixin, GenericViewSet + ListModelMixin, + RetrieveModelMixin, + CreateModelMixin, + UpdateModelMixin, + GenericViewSet, ): queryset = ( PatientNotes.objects.all() .select_related("facility", "patient", "created_by") .order_by("-created_date") ) + http_method_names = ["get", "post", "put", "patch"] serializer_class = PatientNotesSerializer permission_classes = (IsAuthenticated, DRYPermissions) @@ -634,6 +644,7 @@ def get_queryset(self): return queryset def perform_create(self, serializer): + serializer.validated_data.pop("edit_window") patient = get_object_or_404( get_patient_notes_queryset(self.request.user).filter( external_id=self.kwargs.get("patient_external_id") @@ -648,3 +659,23 @@ def perform_create(self, serializer): patient=patient, created_by=self.request.user, ) + + def perform_update(self, serializer): + edit_window = serializer.validated_data.get("edit_window") + note = serializer.instance + time_diff = now() - note.created_date + max_update_time = datetime.timedelta(seconds=edit_window) + if time_diff > max_update_time: + raise ValidationError( + {"note": f"Note can not be updated after {edit_window / 60:.2f} mins"} + ) + patient = get_object_or_404( + get_patient_notes_queryset(self.request.user).filter( + external_id=self.kwargs.get("patient_external_id") + ) + ) + if not patient.is_active: + raise ValidationError( + {"patient": "Only active patients data can be updated"} + ) + return super().perform_update(serializer) diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index 43c0524234..6291c686f8 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -22,20 +22,33 @@ def has_write_permission(request): def has_object_read_permission(self, request): doctor_allowed = False if self.last_consultation: - doctor_allowed = self.last_consultation.assigned_to == request.user or request.user == self.assigned_to + doctor_allowed = ( + self.last_consultation.assigned_to == request.user + or request.user == self.assigned_to + ) return request.user.is_superuser or ( (hasattr(self, "created_by") and request.user == self.created_by) - or (self.facility and request.user in self.facility.users.all() or doctor_allowed) + or ( + self.facility + and request.user in self.facility.users.all() + or doctor_allowed + ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] and ( request.user.district == self.district - or (self.facility and request.user.district == self.facility.district) + or ( + self.facility + and request.user.district == self.facility.district + ) ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - and (request.user.state == self.state or (self.facility and request.user.state == self.facility.state)) + and ( + request.user.state == self.state + or (self.facility and request.user.state == self.facility.state) + ) ) ) @@ -44,7 +57,10 @@ def has_object_write_permission(self, request): return False doctor_allowed = False if self.last_consultation: - doctor_allowed = self.last_consultation.assigned_to == request.user or request.user == self.assigned_to + doctor_allowed = ( + self.last_consultation.assigned_to == request.user + or request.user == self.assigned_to + ) if ( request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] @@ -60,12 +76,18 @@ def has_object_write_permission(self, request): request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] and ( request.user.district == self.district - or (self.facility and request.user.district == self.facility.district) + or ( + self.facility + and request.user.district == self.facility.district + ) ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - and (request.user.state == self.state or (self.facility and request.user.state == self.facility.state)) + and ( + request.user.state == self.state + or (self.facility and request.user.state == self.facility.state) + ) ) ) @@ -74,7 +96,10 @@ def has_object_update_permission(self, request): return False doctor_allowed = False if self.last_consultation: - doctor_allowed = self.last_consultation.assigned_to == request.user or request.user == self.assigned_to + doctor_allowed = ( + self.last_consultation.assigned_to == request.user + or request.user == self.assigned_to + ) if ( request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] @@ -90,12 +115,18 @@ def has_object_update_permission(self, request): request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] and ( request.user.district == self.district - or (self.facility and request.user.district == self.facility.district) + or ( + self.facility + and request.user.district == self.facility.district + ) ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - and (request.user.state == self.state or (self.facility and request.user.state == self.facility.state)) + and ( + request.user.state == self.state + or (self.facility and request.user.state == self.facility.state) + ) ) ) @@ -111,8 +142,12 @@ def has_object_transfer_permission(self, request): or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] ): return False - new_facility = Facility.objects.filter(id=request.data.get("facility", None)).first() - return self.has_object_update_permission(request) or (new_facility and request.user in new_facility.users.all()) + new_facility = Facility.objects.filter( + id=request.data.get("facility", None) + ).first() + return self.has_object_update_permission(request) or ( + new_facility and request.user in new_facility.users.all() + ) class PatientRelatedPermissionMixin(BasePermissionMixin): @@ -133,15 +168,28 @@ def has_write_permission(request): def has_object_read_permission(self, request): return ( request.user.is_superuser - or (self.patient.facility and request.user in self.patient.facility.users.all()) - or (self.assigned_to == request.user or request.user == self.patient.assigned_to) + or ( + self.patient.facility + and request.user in self.patient.facility.users.all() + ) + or ( + getattr(self, "assigned_to", None) + and getattr(self, "assigned_to", None) == request.user + ) + or request.user == getattr(self.patient, "assigned_to", None) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] - and (self.patient.facility and request.user.district == self.patient.facility.district) + and ( + self.patient.facility + and request.user.district == self.patient.facility.district + ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - and (self.patient.facility and request.user.state == self.patient.facility.state) + and ( + self.patient.facility + and request.user.state == self.patient.facility.state + ) ) ) @@ -154,14 +202,26 @@ def has_object_update_permission(self, request): return False return ( request.user.is_superuser - or (self.patient.facility and self.patient.facility == request.user.home_facility) - or (self.assigned_to == request.user or request.user == self.patient.assigned_to) + or ( + self.patient.facility + and self.patient.facility == request.user.home_facility + ) + or ( + getattr(self, "assigned_to", None) + and getattr(self, "assigned_to", None) == request.user + ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] - and (self.patient.facility and request.user.district == self.patient.facility.district) + and ( + self.patient.facility + and request.user.district == self.patient.facility.district + ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - and (self.patient.facility and request.user.state == self.patient.facility.state) + and ( + self.patient.facility + and request.user.state == self.patient.facility.state + ) ) ) From b68cdb49f72df89f7a1adf57330ec14ff12cb559 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 18 Apr 2023 19:59:42 +0530 Subject: [PATCH 02/31] Use editable_until logic --- care/facility/api/serializers/patient.py | 20 ++++------------ care/facility/api/viewsets/patient.py | 24 +------------------ .../0336_patientnotes_editable_until.py | 18 ++++++++++++++ care/facility/models/patient.py | 24 +++++++++++++++++++ 4 files changed, 48 insertions(+), 38 deletions(-) create mode 100644 care/facility/migrations/0336_patientnotes_editable_until.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 581a335c2d..159b57ce97 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -1,6 +1,7 @@ import datetime from django.db import transaction +from django.db.models import Q from django.utils.timezone import localtime, make_aware, now from rest_framework import serializers @@ -33,6 +34,8 @@ from care.facility.models.patient_consultation import PatientConsultation from care.facility.models.patient_external_test import PatientExternalTest from care.facility.models.patient_tele_consultation import PatientTeleConsultation +from care.hcx.models.claim import Claim +from care.hcx.models.policy import Policy from care.users.api.serializers.lsg import ( DistrictSerializer, LocalBodySerializer, @@ -48,9 +51,6 @@ PhoneNumberIsPossibleField, ) from config.serializers import ChoiceField -from care.hcx.models.policy import Policy -from care.hcx.models.claim import Claim -from django.db.models import Q class PatientMetaInfoSerializer(serializers.ModelSerializer): @@ -457,18 +457,8 @@ def save(self, **kwargs): class PatientNotesSerializer(serializers.ModelSerializer): - NOTE_EDIT_WINDOW = 30 * 60 # 30 minutes - facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) - edit_window = serializers.SerializerMethodField(read_only=True) - - def get_edit_window(self, obj): - return self.NOTE_EDIT_WINDOW - - def validate(self, data): - data["edit_window"] = self.get_edit_window(data) - return data def validate_empty_values(self, data): if not data.get("note", "").strip(): @@ -484,6 +474,6 @@ class Meta: "created_by_object", "created_date", "modified_date", - "edit_window", + "editable_until", ) - read_only_fields = ("id", "created_date", "modified_date") + read_only_fields = ("id", "created_date", "modified_date", "editable_until") diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 47e0b2e3ad..44ea2a767f 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -676,7 +676,6 @@ class PatientNotesViewSet( .select_related("facility", "patient", "created_by") .order_by("-created_date") ) - http_method_names = ["get", "post", "put", "patch"] serializer_class = PatientNotesSerializer permission_classes = (IsAuthenticated, DRYPermissions) @@ -700,7 +699,6 @@ def get_queryset(self): return queryset def perform_create(self, serializer): - serializer.validated_data.pop("edit_window") patient = get_object_or_404( get_patient_notes_queryset(self.request.user).filter( external_id=self.kwargs.get("patient_external_id") @@ -708,30 +706,10 @@ def perform_create(self, serializer): ) if not patient.is_active: raise ValidationError( - {"patient": "Only active patients data can be updated"} + {"patient": "Updating patient data is only allowed for active patients"} ) return serializer.save( facility=patient.facility, patient=patient, created_by=self.request.user, ) - - def perform_update(self, serializer): - edit_window = serializer.validated_data.get("edit_window") - note = serializer.instance - time_diff = now() - note.created_date - max_update_time = datetime.timedelta(seconds=edit_window) - if time_diff > max_update_time: - raise ValidationError( - {"note": f"Note can not be updated after {edit_window / 60:.2f} mins"} - ) - patient = get_object_or_404( - get_patient_notes_queryset(self.request.user).filter( - external_id=self.kwargs.get("patient_external_id") - ) - ) - if not patient.is_active: - raise ValidationError( - {"patient": "Only active patients data can be updated"} - ) - return super().perform_update(serializer) diff --git a/care/facility/migrations/0336_patientnotes_editable_until.py b/care/facility/migrations/0336_patientnotes_editable_until.py new file mode 100644 index 0000000000..aaafa27959 --- /dev/null +++ b/care/facility/migrations/0336_patientnotes_editable_until.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2023-04-18 13:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0335_auto_20230207_1914'), + ] + + operations = [ + migrations.AddField( + model_name='patientnotes', + name='editable_until', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 78a30d2fca..45ca40f983 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -1,8 +1,10 @@ import datetime import enum +from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.utils.timezone import now from fernet_fields import EncryptedCharField, EncryptedIntegerField from partial_index import PQ, PartialIndex from simple_history.models import HistoricalRecords @@ -745,6 +747,8 @@ class PatientMobileOTP(BaseModel): class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin): + EDIT_WINDOW_DURATION = 30 * 60 # 30 minutes + patient = models.ForeignKey( PatientRegistration, on_delete=models.PROTECT, null=False, blank=False ) @@ -757,3 +761,23 @@ class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin): null=True, ) note = models.TextField(default="", blank=True) + + editable_until = models.DateTimeField(null=True, blank=True) + + def save(self, *args, **kwargs): + if not self.pk: # creating new instance + if not self.editable_until: + self.editable_until = now() + datetime.timedelta( + seconds=self.EDIT_WINDOW_DURATION + ) + else: # updating existing instance + if not self.patient.is_active: + raise ValidationError( + { + "patient": "Updating patient data is only allowed for active patients" + } + ) + if not self.editable_until or now() > self.editable_until: + raise ValidationError({"note": "Note is not editable anymore"}) + + super().save(*args, **kwargs) From d6a3574807befb99647b74fce69e9fd5aaf98610 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 18 Apr 2023 20:04:50 +0530 Subject: [PATCH 03/31] dummy empty commit From 18dc937dbd828cc4b2ff62501dda42618d5fc01b Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 19 Jul 2023 17:20:55 +0530 Subject: [PATCH 04/31] Merge migrations --- care/facility/migrations/0372_merge_20230719_1719.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 care/facility/migrations/0372_merge_20230719_1719.py diff --git a/care/facility/migrations/0372_merge_20230719_1719.py b/care/facility/migrations/0372_merge_20230719_1719.py new file mode 100644 index 0000000000..9e993de20f --- /dev/null +++ b/care/facility/migrations/0372_merge_20230719_1719.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.2 on 2023-07-19 11:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0336_patientnotes_editable_until"), + ("facility", "0371_metaicd11diagnosis_chapter_and_more"), + ] + + operations = [] From 11b91dfb260ab40ccc73ba65d732a69d90ed1bd2 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 19 Jul 2023 17:24:46 +0530 Subject: [PATCH 05/31] format files --- .../migrations/0336_patientnotes_editable_until.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/care/facility/migrations/0336_patientnotes_editable_until.py b/care/facility/migrations/0336_patientnotes_editable_until.py index aaafa27959..499eb75843 100644 --- a/care/facility/migrations/0336_patientnotes_editable_until.py +++ b/care/facility/migrations/0336_patientnotes_editable_until.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('facility', '0335_auto_20230207_1914'), + ("facility", "0335_auto_20230207_1914"), ] operations = [ migrations.AddField( - model_name='patientnotes', - name='editable_until', + model_name="patientnotes", + name="editable_until", field=models.DateTimeField(blank=True, null=True), ), ] From 3d85cc41e797b40ca1380a99d36b15f2033e1cb0 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 19 Jul 2023 17:30:19 +0530 Subject: [PATCH 06/31] Merge migrations --- care/facility/migrations/0373_merge_20230719_1729.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 care/facility/migrations/0373_merge_20230719_1729.py diff --git a/care/facility/migrations/0373_merge_20230719_1729.py b/care/facility/migrations/0373_merge_20230719_1729.py new file mode 100644 index 0000000000..a5c0b7c901 --- /dev/null +++ b/care/facility/migrations/0373_merge_20230719_1729.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.2 on 2023-07-19 11:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0372_assetavailabilityrecord"), + ("facility", "0372_merge_20230719_1719"), + ] + + operations = [] From 85531fe1912dcd47d7666782c94f508c07e4bf6c Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:01:41 +0530 Subject: [PATCH 07/31] fix tests --- care/facility/tests/test_patient_api.py | 3 +++ care/utils/tests/test_base.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 86d96ade62..cb86f8c803 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -9,10 +9,13 @@ class ExpectedPatientNoteKeys(Enum): + ID = "id" NOTE = "note" FACILITY = "facility" CREATED_BY_OBJECT = "created_by_object" CREATED_DATE = "created_date" + EDITABLE_UNTIL = "editable_until" + MODIFIED_DATE = "modified_date" class ExpectedFacilityKeys(Enum): diff --git a/care/utils/tests/test_base.py b/care/utils/tests/test_base.py index 3ed212c1b2..3342238ab6 100644 --- a/care/utils/tests/test_base.py +++ b/care/utils/tests/test_base.py @@ -439,7 +439,7 @@ def create_consultation( return PatientConsultation.objects.create(**data) def create_patient_note( - self, patient=None, facility=None, note="Patient is doing find", **kwargs + self, patient=None, facility=None, note="Patient is doing fine", **kwargs ): data = { "patient": patient or self.patient, From aec33b502c0d2ab997856cc913651104fb2dfd20 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:40:05 +0530 Subject: [PATCH 08/31] remove editable_until --- care/facility/api/serializers/patient.py | 8 ++++++-- .../0336_patientnotes_editable_until.py | 17 ----------------- .../migrations/0372_merge_20230719_1719.py | 12 ------------ .../migrations/0373_merge_20230719_1729.py | 12 ------------ care/facility/models/patient.py | 19 +++++++------------ care/facility/tests/test_patient_api.py | 1 - 6 files changed, 13 insertions(+), 56 deletions(-) delete mode 100644 care/facility/migrations/0336_patientnotes_editable_until.py delete mode 100644 care/facility/migrations/0372_merge_20230719_1719.py delete mode 100644 care/facility/migrations/0373_merge_20230719_1729.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 20a4cad77f..e9c3e61f56 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -26,6 +26,7 @@ PatientRegistration, ) from care.facility.models.notification import Notification +from care.facility.models.patient import PATIENT_NOTE_EDIT_WINDOW from care.facility.models.patient_base import ( BLOOD_GROUP_CHOICES, DISEASE_STATUS_CHOICES, @@ -455,6 +456,10 @@ def save(self, **kwargs): class PatientNotesSerializer(serializers.ModelSerializer): facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) + edit_window_seconds = serializers.SerializerMethodField() + + def get_edit_window_seconds(self, obj): + return PATIENT_NOTE_EDIT_WINDOW def validate_empty_values(self, data): if not data.get("note", "").strip(): @@ -470,6 +475,5 @@ class Meta: "created_by_object", "created_date", "modified_date", - "editable_until", ) - read_only_fields = ("id", "created_date", "modified_date", "editable_until") + read_only_fields = ("id", "created_date", "modified_date") diff --git a/care/facility/migrations/0336_patientnotes_editable_until.py b/care/facility/migrations/0336_patientnotes_editable_until.py deleted file mode 100644 index 499eb75843..0000000000 --- a/care/facility/migrations/0336_patientnotes_editable_until.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2023-04-18 13:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0335_auto_20230207_1914"), - ] - - operations = [ - migrations.AddField( - model_name="patientnotes", - name="editable_until", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/care/facility/migrations/0372_merge_20230719_1719.py b/care/facility/migrations/0372_merge_20230719_1719.py deleted file mode 100644 index 9e993de20f..0000000000 --- a/care/facility/migrations/0372_merge_20230719_1719.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.2 on 2023-07-19 11:49 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0336_patientnotes_editable_until"), - ("facility", "0371_metaicd11diagnosis_chapter_and_more"), - ] - - operations = [] diff --git a/care/facility/migrations/0373_merge_20230719_1729.py b/care/facility/migrations/0373_merge_20230719_1729.py deleted file mode 100644 index a5c0b7c901..0000000000 --- a/care/facility/migrations/0373_merge_20230719_1729.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.2 on 2023-07-19 11:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0372_assetavailabilityrecord"), - ("facility", "0372_merge_20230719_1719"), - ] - - operations = [] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index b6f210440c..aed93db74e 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -43,6 +43,8 @@ ) from care.utils.models.base import BaseManager, BaseModel +PATIENT_NOTE_EDIT_WINDOW = 30 * 60 # 30 minutes + class PatientRegistration(PatientBaseModel, PatientPermissionMixin): # fields in the PatientSearch model @@ -673,8 +675,6 @@ class PatientMobileOTP(BaseModel): class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin): - EDIT_WINDOW_DURATION = 30 * 60 # 30 minutes - patient = models.ForeignKey( PatientRegistration, on_delete=models.PROTECT, null=False, blank=False ) @@ -688,22 +688,17 @@ class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin): ) note = models.TextField(default="", blank=True) - editable_until = models.DateTimeField(null=True, blank=True) - def save(self, *args, **kwargs): - if not self.pk: # creating new instance - if not self.editable_until: - self.editable_until = now() + datetime.timedelta( - seconds=self.EDIT_WINDOW_DURATION - ) - else: # updating existing instance + if self.pk: # Updating an existing note if not self.patient.is_active: raise ValidationError( { "patient": "Updating patient data is only allowed for active patients" } ) - if not self.editable_until or now() > self.editable_until: + if now() > self.created_date + datetime.timedelta( + seconds=PATIENT_NOTE_EDIT_WINDOW + ): raise ValidationError({"note": "Note is not editable anymore"}) - super().save(*args, **kwargs) + super().save(*args, **kwargs) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index cb86f8c803..c0fea3ce6c 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -14,7 +14,6 @@ class ExpectedPatientNoteKeys(Enum): FACILITY = "facility" CREATED_BY_OBJECT = "created_by_object" CREATED_DATE = "created_date" - EDITABLE_UNTIL = "editable_until" MODIFIED_DATE = "modified_date" From 551d4878a8e9e90da5f0743e6838b041bb4f571e Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Fri, 21 Jul 2023 14:24:20 +0530 Subject: [PATCH 09/31] bug fixes --- care/facility/api/serializers/patient.py | 12 ++++++++---- care/facility/models/patient.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index c691e4ee65..de3549a0a6 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -457,7 +457,7 @@ class PatientNotesSerializer(serializers.ModelSerializer): facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) created_by_local_user = serializers.BooleanField(read_only=True) - edit_window_seconds = serializers.SerializerMethodField() + edit_window_seconds = serializers.SerializerMethodField(read_only=True) def get_edit_window_seconds(self, obj): return PATIENT_NOTE_EDIT_WINDOW @@ -474,7 +474,11 @@ class Meta: "note", "facility", "created_by_object", - "created_date", - "modified_date", + "created_by_local_user", + "edit_window_seconds", + ) + read_only_fields = ( + "id", + "created_by_local_user", + "edit_window_seconds", ) - read_only_fields = ("id", "created_date", "modified_date") diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index aed93db74e..ca82ed4b1c 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -701,4 +701,4 @@ def save(self, *args, **kwargs): ): raise ValidationError({"note": "Note is not editable anymore"}) - super().save(*args, **kwargs) + super().save(*args, **kwargs) From 548b11e4ebbe16a5bc99db79f15e0c9ceaddb83e Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:25:08 +0530 Subject: [PATCH 10/31] Move update validation to viewset --- care/facility/api/serializers/patient.py | 1 + care/facility/api/viewsets/patient.py | 20 ++++++++++++++++++++ care/facility/models/patient.py | 17 ----------------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index de3549a0a6..6c6a40ae05 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -454,6 +454,7 @@ def save(self, **kwargs): class PatientNotesSerializer(serializers.ModelSerializer): + id = serializers.CharField(source="external_id", read_only=True) facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) created_by_local_user = serializers.BooleanField(read_only=True) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 87a73f154b..055f8a463d 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -7,6 +7,7 @@ from django.db import models from django.db.models import BooleanField, Case, F, Value, When from django.db.models.query_utils import Q +from django.utils.timezone import now from django_filters import rest_framework as filters from djqscsv import render_to_csv_response from drf_spectacular.utils import extend_schema, extend_schema_view @@ -53,6 +54,7 @@ ) from care.facility.models.base import covert_choice_dict from care.facility.models.bed import AssetBed +from care.facility.models.patient import PATIENT_NOTE_EDIT_WINDOW from care.facility.models.patient_base import DISEASE_STATUS_DICT from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities @@ -616,6 +618,7 @@ class PatientNotesViewSet( ) .order_by("-created_date") ) + lookup_field = "external_id" serializer_class = PatientNotesSerializer permission_classes = (IsAuthenticated, DRYPermissions) @@ -654,3 +657,20 @@ def perform_create(self, serializer): patient=patient, created_by=self.request.user, ) + + def perform_update(self, serializer): + user = self.request.user + patient_note = serializer.instance + if not user.is_superuser: + if not patient_note.patient.is_active: + raise ValidationError( + { + "patient": "Updating patient data is only allowed for active patients" + } + ) + if now() > patient_note.created_date + datetime.timedelta( + seconds=PATIENT_NOTE_EDIT_WINDOW + ): + raise ValidationError({"note": "Note is not editable anymore"}) + validated_data = serializer.validated_data + serializer.save(**validated_data) diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index ca82ed4b1c..2780ddcea3 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -1,11 +1,9 @@ import datetime import enum -from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import JSONField -from django.utils.timezone import now from simple_history.models import HistoricalRecords from care.facility.models import ( @@ -687,18 +685,3 @@ class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin): null=True, ) note = models.TextField(default="", blank=True) - - def save(self, *args, **kwargs): - if self.pk: # Updating an existing note - if not self.patient.is_active: - raise ValidationError( - { - "patient": "Updating patient data is only allowed for active patients" - } - ) - if now() > self.created_date + datetime.timedelta( - seconds=PATIENT_NOTE_EDIT_WINDOW - ): - raise ValidationError({"note": "Note is not editable anymore"}) - - super().save(*args, **kwargs) From c415185f718765482961412001595e320ea04f23 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:37:10 +0530 Subject: [PATCH 11/31] Fix tests --- care/facility/api/serializers/patient.py | 4 ++++ care/facility/tests/test_patient_api.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 48efd0db8c..4f63676877 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -483,10 +483,14 @@ class Meta: "facility", "created_by_object", "created_by_local_user", + "created_date", + "modified_date", "edit_window_seconds", ) read_only_fields = ( "id", "created_by_local_user", + "created_date", + "modified_date", "edit_window_seconds", ) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 523b91bbd6..e60ce4206c 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -13,9 +13,10 @@ class ExpectedPatientNoteKeys(Enum): NOTE = "note" FACILITY = "facility" CREATED_BY_OBJECT = "created_by_object" + CREATED_BY_LOCAL_USER = "created_by_local_user" + EDIT_WINDOW_SECONDS = "edit_window_seconds" CREATED_DATE = "created_date" MODIFIED_DATE = "modified_date" - CREATED_BY_LOCAL_USER = "created_by_local_user" class ExpectedFacilityKeys(Enum): From 4dd8d12ffaa9136a55ed620909d65d64cedcdb9f Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:15:08 +0530 Subject: [PATCH 12/31] Remove PATIENT_NOTE_EDIT_WINDOW --- care/facility/api/serializers/patient.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 4f63676877..ec83c8c9ec 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -465,10 +465,6 @@ class PatientNotesSerializer(serializers.ModelSerializer): facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) created_by_local_user = serializers.BooleanField(read_only=True) - edit_window_seconds = serializers.SerializerMethodField(read_only=True) - - def get_edit_window_seconds(self, obj): - return PATIENT_NOTE_EDIT_WINDOW def validate_empty_values(self, data): if not data.get("note", "").strip(): From 2e90292ee06191e22c4ed58f695f37cdd475b367 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:19:35 +0530 Subject: [PATCH 13/31] fix lint --- care/facility/api/serializers/patient.py | 1 - 1 file changed, 1 deletion(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index ec83c8c9ec..3741b4993f 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -28,7 +28,6 @@ PatientRegistration, ) from care.facility.models.notification import Notification -from care.facility.models.patient import PATIENT_NOTE_EDIT_WINDOW from care.facility.models.patient_base import ( BLOOD_GROUP_CHOICES, DISEASE_STATUS_CHOICES, From 067d147753579e1c6ff570ee234af3a1bdc1ba96 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:06:43 +0530 Subject: [PATCH 14/31] edit history for patient notes --- care/facility/api/serializers/patient.py | 47 ++++++++++++++++-- care/facility/api/viewsets/patient.py | 19 ------- .../migrations/0383_patientnotesedit.py | 49 +++++++++++++++++++ care/facility/models/patient.py | 21 +++++++- care/facility/tests/test_patient_api.py | 1 + 5 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 care/facility/migrations/0383_patientnotesedit.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index b4fd3ffab8..8a858b6961 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -28,6 +28,7 @@ PatientRegistration, ) from care.facility.models.notification import Notification +from care.facility.models.patient import PatientNotesEdit from care.facility.models.patient_base import ( BLOOD_GROUP_CHOICES, DISEASE_STATUS_CHOICES, @@ -459,10 +460,20 @@ def save(self, **kwargs): self.instance.save() +class PatientNotesEditSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="patient_note.external_id", read_only=True) + edited_by = UserBaseMinimumSerializer(read_only=True) + + class Meta: + model = PatientNotesEdit + exclude = ("patient_note",) + + class PatientNotesSerializer(serializers.ModelSerializer): id = serializers.CharField(source="external_id", read_only=True) facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) + edits = PatientNotesEditSerializer(many=True, read_only=True) def validate_empty_values(self, data): if not data.get("note", "").strip(): @@ -482,7 +493,37 @@ def create(self, validated_data): # If the user is not a doctor then the user type is the same as the user type validated_data["user_type"] = user_type - return super().create(validated_data) + user = self.context["request"].user + note = validated_data.get("note") + with transaction.atomic(): + instance = super().create(validated_data) + initial_edit = PatientNotesEdit( + patient_note=instance, + edited_on=now(), + edited_by=user, + note=note, + ) + initial_edit.save() + + return instance + + def update(self, instance, validated_data): + user = self.context["request"].user + note = validated_data.get("note") + + if note == instance.note: + return instance + + with transaction.atomic(): + edit = PatientNotesEdit( + patient_note=instance, + edited_on=now(), + edited_by=user, + note=note, + ) + edit.save() + + return super().update(instance, validated_data) class Meta: model = PatientNotes @@ -494,12 +535,12 @@ class Meta: "user_type", "created_date", "modified_date", - "edit_window_seconds", + "edits", ) read_only_fields = ( "id", "created_by_local_user", "created_date", "modified_date", - "edit_window_seconds", + "edits", ) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index ab80208c57..b1d3dbb940 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -7,7 +7,6 @@ from django.db import models from django.db.models import Case, When from django.db.models.query_utils import Q -from django.utils.timezone import now from django_filters import rest_framework as filters from djqscsv import render_to_csv_response from drf_spectacular.utils import extend_schema, extend_schema_view @@ -54,7 +53,6 @@ ) from care.facility.models.base import covert_choice_dict from care.facility.models.bed import AssetBed -from care.facility.models.patient import PATIENT_NOTE_EDIT_WINDOW from care.facility.models.patient_base import DISEASE_STATUS_DICT from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities @@ -647,20 +645,3 @@ def perform_create(self, serializer): patient=patient, created_by=self.request.user, ) - - def perform_update(self, serializer): - user = self.request.user - patient_note = serializer.instance - if not user.is_superuser: - if not patient_note.patient.is_active: - raise ValidationError( - { - "patient": "Updating patient data is only allowed for active patients" - } - ) - if now() > patient_note.created_date + datetime.timedelta( - seconds=PATIENT_NOTE_EDIT_WINDOW - ): - raise ValidationError({"note": "Note is not editable anymore"}) - validated_data = serializer.validated_data - serializer.save(**validated_data) diff --git a/care/facility/migrations/0383_patientnotesedit.py b/care/facility/migrations/0383_patientnotesedit.py new file mode 100644 index 0000000000..da9dc0e97d --- /dev/null +++ b/care/facility/migrations/0383_patientnotesedit.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.2 on 2023-08-28 14:09 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0382_assetservice_remove_asset_last_serviced_on_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="PatientNotesEdit", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("edited_on", models.DateTimeField(auto_now_add=True)), + ("note", models.TextField()), + ( + "edited_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "patient_note", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="edits", + to="facility.patientnotes", + ), + ), + ], + options={ + "ordering": ["-edited_on"], + }, + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index d50bff793c..aa34271cc9 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -38,8 +38,6 @@ from care.utils.models.base import BaseManager, BaseModel from care.utils.models.validators import mobile_or_landline_number_validator -PATIENT_NOTE_EDIT_WINDOW = 30 * 60 # 30 minutes - class PatientRegistration(PatientBaseModel, PatientPermissionMixin): # fields in the PatientSearch model @@ -690,3 +688,22 @@ class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin): null=True, ) note = models.TextField(default="", blank=True) + + +class PatientNotesEdit(models.Model): + patient_note = models.ForeignKey( + PatientNotes, + on_delete=models.CASCADE, + null=False, + blank=False, + related_name="edits", + ) + edited_on = models.DateTimeField(auto_now_add=True) + edited_by = models.ForeignKey( + User, on_delete=models.PROTECT, null=False, blank=False + ) + + note = models.TextField() + + class Meta: + ordering = ["-edited_on"] diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 0c37d2e6dd..7c7e60cf98 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -16,6 +16,7 @@ class ExpectedPatientNoteKeys(Enum): NOTE = "note" FACILITY = "facility" CREATED_BY_OBJECT = "created_by_object" + CREATED_BY_LOCAL_USER = "created_by_local_user" CREATED_DATE = "created_date" USER_TYPE = "user_type" From 4eaab8b3dc0900afaa0d6b7bbbd1ddc55ef1362e Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:54:18 +0530 Subject: [PATCH 15/31] Fix tests --- care/facility/api/serializers/patient.py | 1 - care/facility/tests/test_patient_api.py | 25 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 8a858b6961..631d439b1b 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -539,7 +539,6 @@ class Meta: ) read_only_fields = ( "id", - "created_by_local_user", "created_date", "modified_date", "edits", diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 7c7e60cf98..8cdd712d5d 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -16,8 +16,9 @@ class ExpectedPatientNoteKeys(Enum): NOTE = "note" FACILITY = "facility" CREATED_BY_OBJECT = "created_by_object" - CREATED_BY_LOCAL_USER = "created_by_local_user" CREATED_DATE = "created_date" + MODIFIED_DATE = "modified_date" + EDITS = "edits" USER_TYPE = "user_type" @@ -200,6 +201,28 @@ def test_patient_notes(self): [item.value for item in ExpectedCreatedByObjectKeys], ) + def test_patient_note_edit(self): + patientId = self.patient.external_id + response = self.client.get(f"/api/v1/patient/{patientId}/notes/") + + data = response.json()["results"][0] + self.assertEqual(len(data["edits"]), 1) + + note_id = data["id"] + note_content = data["note"] + new_note_content = note_content + " edited" + + response = self.client.put( + f"/api/v1/patient/{patientId}/notes/{note_id}/", {"note": new_note_content} + ) + updated_data = response.json() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(updated_data["note"], new_note_content) + self.assertEqual(len(updated_data["edits"]), 2) + self.assertEqual(updated_data["edits"][0]["note"], new_note_content) + self.assertEqual(updated_data["edits"][1]["note"], note_content) + class PatientFilterTestCase(TestBase, TestClassMixin, APITestCase): def setUp(self): From 3587a870a02607205115204cddbc8cb2bf4faedb Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:15:24 +0530 Subject: [PATCH 16/31] Create initial edit record for existing notes --- .../migrations/0383_patientnotesedit.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/care/facility/migrations/0383_patientnotesedit.py b/care/facility/migrations/0383_patientnotesedit.py index da9dc0e97d..1ec0ac87ef 100644 --- a/care/facility/migrations/0383_patientnotesedit.py +++ b/care/facility/migrations/0383_patientnotesedit.py @@ -5,6 +5,28 @@ from django.db import migrations, models +def create_initial_patient_notes_edit_record(apps, schema_editor): + PatientNotes = apps.get_model("facility", "PatientNotes") + PatientNotesEdit = apps.get_model("facility", "PatientNotesEdit") + + edit_records = [] + + for patient_note in PatientNotes.objects.all(): + if ( + not patient_note.edits.all() # Check to not re-create records when rolling back and re-migrating + ): + edit_record = PatientNotesEdit( + patient_note=patient_note, + edited_on=patient_note.created_date, + edited_by=patient_note.created_by, + note=patient_note.note, + ) + + edit_records.append(edit_record) + + PatientNotesEdit.objects.bulk_create(edit_records) + + class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), @@ -46,4 +68,8 @@ class Migration(migrations.Migration): "ordering": ["-edited_on"], }, ), + migrations.RunPython( + code=create_initial_patient_notes_edit_record, + reverse_code=django.db.migrations.operations.special.RunPython.noop, + ), ] From dc899d0230a91e2624c8c8b95bb986dcb244c4c1 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Tue, 29 Aug 2023 15:28:32 +0530 Subject: [PATCH 17/31] optimize migration Signed-off-by: Aakash Singh --- .../migrations/0383_patientnotesedit.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/care/facility/migrations/0383_patientnotesedit.py b/care/facility/migrations/0383_patientnotesedit.py index 1ec0ac87ef..460c657e01 100644 --- a/care/facility/migrations/0383_patientnotesedit.py +++ b/care/facility/migrations/0383_patientnotesedit.py @@ -2,6 +2,7 @@ import django.db.models.deletion from django.conf import settings +from django.core.paginator import Paginator from django.db import migrations, models @@ -9,12 +10,20 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): PatientNotes = apps.get_model("facility", "PatientNotes") PatientNotesEdit = apps.get_model("facility", "PatientNotesEdit") - edit_records = [] + notes_without_edits = ( + PatientNotes.objects.annotate( + has_edits=models.Exists( + PatientNotesEdit.objects.filter(patient_note=models.OuterRef("pk")) + ) + ) + .filter(has_edits=False) + .order_by("id") + ) - for patient_note in PatientNotes.objects.all(): - if ( - not patient_note.edits.all() # Check to not re-create records when rolling back and re-migrating - ): + paginator = Paginator(notes_without_edits, 1000) + for page_number in paginator.page_range: + edit_records = [] + for patient_note in paginator.page(page_number).object_list: edit_record = PatientNotesEdit( patient_note=patient_note, edited_on=patient_note.created_date, @@ -24,7 +33,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): edit_records.append(edit_record) - PatientNotesEdit.objects.bulk_create(edit_records) + PatientNotesEdit.objects.bulk_create(edit_records) class Migration(migrations.Migration): @@ -70,6 +79,6 @@ class Migration(migrations.Migration): ), migrations.RunPython( code=create_initial_patient_notes_edit_record, - reverse_code=django.db.migrations.operations.special.RunPython.noop, + reverse_code=migrations.RunPython.noop, ), ] From 7e44d7a377dd3a1a10a6436f75763ffc9bca1c07 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:54:46 +0530 Subject: [PATCH 18/31] Update care/facility/models/mixins/permissions/patient.py Co-authored-by: Aakash Singh --- care/facility/models/mixins/permissions/patient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index 6291c686f8..0c278948ed 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -210,6 +210,7 @@ def has_object_update_permission(self, request): getattr(self, "assigned_to", None) and getattr(self, "assigned_to", None) == request.user ) + or request.user == getattr(self.patient, "assigned_to", None) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] and ( From f2910552019932dc7235ae05cd6bdb86d6107c3d Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:59:19 +0530 Subject: [PATCH 19/31] Update migrations --- .../{0383_patientnotesedit.py => 0404_patientnotesedit.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename care/facility/migrations/{0383_patientnotesedit.py => 0404_patientnotesedit.py} (97%) diff --git a/care/facility/migrations/0383_patientnotesedit.py b/care/facility/migrations/0404_patientnotesedit.py similarity index 97% rename from care/facility/migrations/0383_patientnotesedit.py rename to care/facility/migrations/0404_patientnotesedit.py index 460c657e01..62d9a4cc48 100644 --- a/care/facility/migrations/0383_patientnotesedit.py +++ b/care/facility/migrations/0404_patientnotesedit.py @@ -39,7 +39,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0382_assetservice_remove_asset_last_serviced_on_and_more"), + ("facility", "0403_alter_dailyround_rounds_type"), ] operations = [ From 0117da4be6d5ced43d09ae040505bd71c75203cc Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:07:32 +0530 Subject: [PATCH 20/31] update migrations --- .../{0404_patientnotesedit.py => 0405_patientnotesedit.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename care/facility/migrations/{0404_patientnotesedit.py => 0405_patientnotesedit.py} (97%) diff --git a/care/facility/migrations/0404_patientnotesedit.py b/care/facility/migrations/0405_patientnotesedit.py similarity index 97% rename from care/facility/migrations/0404_patientnotesedit.py rename to care/facility/migrations/0405_patientnotesedit.py index 62d9a4cc48..12e6ed3168 100644 --- a/care/facility/migrations/0404_patientnotesedit.py +++ b/care/facility/migrations/0405_patientnotesedit.py @@ -39,7 +39,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0403_alter_dailyround_rounds_type"), + ("facility", "0404_merge_20231220_2227"), ] operations = [ From 2d9bf408099f31e943935ffb0c1b7e9b02a4a046 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:15:46 +0530 Subject: [PATCH 21/31] Update care/facility/api/serializers/patient.py Co-authored-by: Aakash Singh --- care/facility/api/serializers/patient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 2ddda71bef..de82dcb2b0 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -518,7 +518,7 @@ def create(self, validated_data): instance = super().create(validated_data) initial_edit = PatientNotesEdit( patient_note=instance, - edited_on=now(), + edited_on=instance.modified_date, edited_by=user, note=note, ) From d8050c29922e4fb838a1475a50a389cb8a524838 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:15:57 +0530 Subject: [PATCH 22/31] Update care/facility/models/patient.py Co-authored-by: Aakash Singh --- care/facility/models/patient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index b490c8e63c..8ac1257da3 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -743,7 +743,7 @@ class PatientNotesEdit(models.Model): blank=False, related_name="edits", ) - edited_on = models.DateTimeField(auto_now_add=True) + edited_date = models.DateTimeField(auto_now_add=True) edited_by = models.ForeignKey( User, on_delete=models.PROTECT, null=False, blank=False ) From 1e50d34ba414d2523e3080c0f7b12d738769829a Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 27 Dec 2023 14:21:23 +0530 Subject: [PATCH 23/31] Update care/facility/migrations/0405_patientnotesedit.py --- care/facility/migrations/0405_patientnotesedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/migrations/0405_patientnotesedit.py b/care/facility/migrations/0405_patientnotesedit.py index 12e6ed3168..40cc7602cd 100644 --- a/care/facility/migrations/0405_patientnotesedit.py +++ b/care/facility/migrations/0405_patientnotesedit.py @@ -26,7 +26,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): for patient_note in paginator.page(page_number).object_list: edit_record = PatientNotesEdit( patient_note=patient_note, - edited_on=patient_note.created_date, + edited_date=patient_note.created_date, edited_by=patient_note.created_by, note=patient_note.note, ) From c7d09ea3c48b0d5a9600cb409fd446137bfba1c0 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 27 Dec 2023 14:21:30 +0530 Subject: [PATCH 24/31] Update care/facility/migrations/0405_patientnotesedit.py --- care/facility/migrations/0405_patientnotesedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/migrations/0405_patientnotesedit.py b/care/facility/migrations/0405_patientnotesedit.py index 40cc7602cd..3a5c4df7cd 100644 --- a/care/facility/migrations/0405_patientnotesedit.py +++ b/care/facility/migrations/0405_patientnotesedit.py @@ -55,7 +55,7 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("edited_on", models.DateTimeField(auto_now_add=True)), + ("edited_date", models.DateTimeField(auto_now_add=True)), ("note", models.TextField()), ( "edited_by", From c61d3e9b611b5ffedcad9400ed164ef11f8abf9b Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:21:31 +0530 Subject: [PATCH 25/31] edited_date --- care/facility/api/serializers/patient.py | 4 ++-- care/facility/migrations/0405_patientnotesedit.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index de82dcb2b0..3924752a6a 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -518,7 +518,7 @@ def create(self, validated_data): instance = super().create(validated_data) initial_edit = PatientNotesEdit( patient_note=instance, - edited_on=instance.modified_date, + edited_date=instance.modified_date, edited_by=user, note=note, ) @@ -536,7 +536,7 @@ def update(self, instance, validated_data): with transaction.atomic(): edit = PatientNotesEdit( patient_note=instance, - edited_on=now(), + edited_date=now(), edited_by=user, note=note, ) diff --git a/care/facility/migrations/0405_patientnotesedit.py b/care/facility/migrations/0405_patientnotesedit.py index 12e6ed3168..f777a8afc5 100644 --- a/care/facility/migrations/0405_patientnotesedit.py +++ b/care/facility/migrations/0405_patientnotesedit.py @@ -26,7 +26,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): for patient_note in paginator.page(page_number).object_list: edit_record = PatientNotesEdit( patient_note=patient_note, - edited_on=patient_note.created_date, + edited_date=patient_note.created_date, edited_by=patient_note.created_by, note=patient_note.note, ) @@ -55,7 +55,7 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("edited_on", models.DateTimeField(auto_now_add=True)), + ("edited_date", models.DateTimeField(auto_now_add=True)), ("note", models.TextField()), ( "edited_by", @@ -74,7 +74,7 @@ class Migration(migrations.Migration): ), ], options={ - "ordering": ["-edited_on"], + "ordering": ["-edited_date"], }, ), migrations.RunPython( From ef69e7e7964f488b7c58d7d265f216a69c54e3f0 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 27 Dec 2023 14:21:38 +0530 Subject: [PATCH 26/31] Update care/facility/migrations/0405_patientnotesedit.py --- care/facility/migrations/0405_patientnotesedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/migrations/0405_patientnotesedit.py b/care/facility/migrations/0405_patientnotesedit.py index 3a5c4df7cd..f777a8afc5 100644 --- a/care/facility/migrations/0405_patientnotesedit.py +++ b/care/facility/migrations/0405_patientnotesedit.py @@ -74,7 +74,7 @@ class Migration(migrations.Migration): ), ], options={ - "ordering": ["-edited_on"], + "ordering": ["-edited_date"], }, ), migrations.RunPython( From a54e2c340b149c05fa9b44681e0277a8d0feb7e4 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 27 Dec 2023 14:21:57 +0530 Subject: [PATCH 27/31] Update care/facility/models/patient.py --- care/facility/models/patient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 8ac1257da3..e65ffe555e 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -751,4 +751,4 @@ class PatientNotesEdit(models.Model): note = models.TextField() class Meta: - ordering = ["-edited_on"] + ordering = ["-edited_date"] From eaf8f68f46efd286315d25d90e183f7a729734b9 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 27 Dec 2023 14:25:05 +0530 Subject: [PATCH 28/31] Apply suggestions from code review --- care/facility/api/serializers/patient.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 3924752a6a..bb768bb2dc 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -534,15 +534,15 @@ def update(self, instance, validated_data): return instance with transaction.atomic(): + instance = super().update(instance, validated_data) edit = PatientNotesEdit( patient_note=instance, - edited_date=now(), + edited_date=instance.modified_date, edited_by=user, note=note, ) edit.save() - - return super().update(instance, validated_data) + return instance class Meta: model = PatientNotes From 59a7b0eba84199689e11e0d685e142219ebbb30d Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:35:29 +0530 Subject: [PATCH 29/31] Fix N+1, update migrations, seperate endpoint for edits --- care/facility/api/serializers/patient.py | 10 ++- care/facility/api/viewsets/patient.py | 75 ++++++++++++++++++- ...tnotesedit.py => 0406_patientnotesedit.py} | 12 +-- config/api_router.py | 6 ++ 4 files changed, 86 insertions(+), 17 deletions(-) rename care/facility/migrations/{0405_patientnotesedit.py => 0406_patientnotesedit.py} (88%) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index c8b02dced6..1dfd5e414b 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -475,7 +475,6 @@ def save(self, **kwargs): class PatientNotesEditSerializer(serializers.ModelSerializer): - id = serializers.UUIDField(source="patient_note.external_id", read_only=True) edited_by = UserBaseMinimumSerializer(read_only=True) class Meta: @@ -487,7 +486,8 @@ class PatientNotesSerializer(serializers.ModelSerializer): id = serializers.CharField(source="external_id", read_only=True) facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) - edits = PatientNotesEditSerializer(many=True, read_only=True) + last_edited_by = serializers.CharField(read_only=True) + last_edited_date = serializers.DateTimeField(read_only=True) consultation = ExternalIdSerializerField( queryset=PatientConsultation.objects.all(), required=False, @@ -556,11 +556,13 @@ class Meta: "user_type", "created_date", "modified_date", - "edits", + "last_edited_by", + "last_edited_date", ) read_only_fields = ( "id", "created_date", "modified_date", - "edits", + "last_edited_by", + "last_edited_date", ) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index c4b5f7b76f..e8f087d691 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -5,8 +5,7 @@ from django.conf import settings from django.contrib.postgres.search import TrigramSimilarity from django.db import models -from django.db.models import Case, When -from django.db.models.query_utils import Q +from django.db.models import Case, OuterRef, Q, Subquery, When from django_filters import rest_framework as filters from djqscsv import render_to_csv_response from drf_spectacular.utils import extend_schema, extend_schema_view @@ -32,6 +31,7 @@ FacilityPatientStatsHistorySerializer, PatientDetailSerializer, PatientListSerializer, + PatientNotesEditSerializer, PatientNotesSerializer, PatientSearchSerializer, PatientTransferSerializer, @@ -54,6 +54,7 @@ from care.facility.models.base import covert_choice_dict from care.facility.models.bed import AssetBed from care.facility.models.notification import Notification +from care.facility.models.patient import PatientNotesEdit from care.facility.models.patient_base import ( DISEASE_STATUS_DICT, NewDischargeReasonEnum, @@ -638,6 +639,43 @@ class PatientNotesFilterSet(filters.FilterSet): consultation = filters.CharFilter(field_name="consultation__external_id") +class PatientNotesEditViewSet( + ListModelMixin, + RetrieveModelMixin, + GenericViewSet, +): + queryset = PatientNotesEdit.objects.all().order_by("-edited_date") + lookup_field = "external_id" + serializer_class = PatientNotesEditSerializer + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + user = self.request.user + + queryset = self.queryset.filter( + patient_note__external_id=self.kwargs.get("notes_external_id") + ) + + if user.is_superuser: + return queryset + if user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter( + patient_note__patient__facility__state=user.state + ) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter( + patient_note__patient__facility__district=user.district + ) + else: + allowed_facilities = get_accessible_facilities(user) + q_filters = Q( + patient_note__patient__facility__id__in=allowed_facilities + ) | Q(patient_note__patient__assigned_to=user) + queryset = queryset.filter(q_filters) + + return queryset + + class PatientNotesViewSet( ListModelMixin, RetrieveModelMixin, @@ -658,10 +696,21 @@ class PatientNotesViewSet( def get_queryset(self): user = self.request.user + + last_edit_subquery = PatientNotesEdit.objects.filter( + patient_note=OuterRef("pk") + ).order_by("-edited_date") + queryset = self.queryset.filter( patient__external_id=self.kwargs.get("patient_external_id") + ).annotate( + last_edited_by=Subquery( + last_edit_subquery.values("edited_by__username")[:1] + ), + last_edited_date=Subquery(last_edit_subquery.values("edited_date")[:1]), ) - if not user.is_superuser: + + if user.is_superuser: return queryset if user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: queryset = queryset.filter(patient__facility__state=user.state) @@ -718,3 +767,23 @@ def perform_create(self, serializer): ).generate() return instance + + def perform_update(self, serializer): + user = self.request.user + patient = get_object_or_404( + get_patient_notes_queryset(self.request.user).filter( + external_id=self.kwargs.get("patient_external_id") + ) + ) + + if not patient.is_active: + raise ValidationError( + {"patient": "Updating patient data is only allowed for active patients"} + ) + + if serializer.instance.created_by != user: + raise ValidationError( + {"Note": "Only the user who created the note can edit it"} + ) + + return super().perform_update(serializer) diff --git a/care/facility/migrations/0405_patientnotesedit.py b/care/facility/migrations/0406_patientnotesedit.py similarity index 88% rename from care/facility/migrations/0405_patientnotesedit.py rename to care/facility/migrations/0406_patientnotesedit.py index f777a8afc5..c1caac7c6c 100644 --- a/care/facility/migrations/0405_patientnotesedit.py +++ b/care/facility/migrations/0406_patientnotesedit.py @@ -10,15 +10,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): PatientNotes = apps.get_model("facility", "PatientNotes") PatientNotesEdit = apps.get_model("facility", "PatientNotesEdit") - notes_without_edits = ( - PatientNotes.objects.annotate( - has_edits=models.Exists( - PatientNotesEdit.objects.filter(patient_note=models.OuterRef("pk")) - ) - ) - .filter(has_edits=False) - .order_by("id") - ) + notes_without_edits = PatientNotes.objects.all() paginator = Paginator(notes_without_edits, 1000) for page_number in paginator.page_range: @@ -39,7 +31,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0404_merge_20231220_2227"), + ("facility", "0405_auto_20231211_1930"), ] operations = [ diff --git a/config/api_router.py b/config/api_router.py index d78f0f568e..410e9eb234 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -44,6 +44,7 @@ from care.facility.api.viewsets.notification import NotificationViewSet from care.facility.api.viewsets.patient import ( FacilityPatientStatsHistoryViewSet, + PatientNotesEditViewSet, PatientNotesViewSet, PatientSearchViewSet, PatientViewSet, @@ -195,6 +196,10 @@ patient_nested_router.register(r"test_sample", PatientSampleViewSet) patient_nested_router.register(r"investigation", PatientInvestigationSummaryViewSet) patient_nested_router.register(r"notes", PatientNotesViewSet) +patient_notes_nested_router = NestedSimpleRouter( + patient_nested_router, r"notes", lookup="notes" +) +patient_notes_nested_router.register(r"edits", PatientNotesEditViewSet) patient_nested_router.register(r"abha", AbhaViewSet) consultation_nested_router = NestedSimpleRouter( @@ -233,6 +238,7 @@ path("", include(facility_nested_router.urls)), path("", include(asset_nested_router.urls)), path("", include(patient_nested_router.urls)), + path("", include(patient_notes_nested_router.urls)), path("", include(consultation_nested_router.urls)), path("", include(resource_nested_router.urls)), path("", include(shifting_nested_router.urls)), From 716368e3a93b5a073d1ed199ddffcf55cd0caa6e Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 23 Jan 2024 01:59:37 +0530 Subject: [PATCH 30/31] Update migrations --- .../{0406_patientnotesedit.py => 0408_patientnotesedit.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename care/facility/migrations/{0406_patientnotesedit.py => 0408_patientnotesedit.py} (97%) diff --git a/care/facility/migrations/0406_patientnotesedit.py b/care/facility/migrations/0408_patientnotesedit.py similarity index 97% rename from care/facility/migrations/0406_patientnotesedit.py rename to care/facility/migrations/0408_patientnotesedit.py index c1caac7c6c..b3743adb55 100644 --- a/care/facility/migrations/0406_patientnotesedit.py +++ b/care/facility/migrations/0408_patientnotesedit.py @@ -31,7 +31,7 @@ def create_initial_patient_notes_edit_record(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0405_auto_20231211_1930"), + ("facility", "0407_alter_dailyround_additional_symptoms_and_more"), ] operations = [ From 3ecf5b6fe9a3c2e3ed720e5bf4807f1b473838c1 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:42:53 +0530 Subject: [PATCH 31/31] Fix tests --- care/facility/api/viewsets/patient.py | 8 ++-- care/facility/tests/test_patient_api.py | 51 +++++++++++++++++++------ 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index e8f087d691..cfedb4be45 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -668,9 +668,10 @@ def get_queryset(self): ) else: allowed_facilities = get_accessible_facilities(user) - q_filters = Q( - patient_note__patient__facility__id__in=allowed_facilities - ) | Q(patient_note__patient__assigned_to=user) + q_filters = Q(patient_note__patient__facility__id__in=allowed_facilities) + q_filters |= Q(patient_note__patient__last_consultation__assigned_to=user) + q_filters |= Q(patient_note__patient__assigned_to=user) + q_filters |= Q(patient_note__created_by=user) queryset = queryset.filter(q_filters) return queryset @@ -721,6 +722,7 @@ def get_queryset(self): q_filters = Q(patient__facility__id__in=allowed_facilities) q_filters |= Q(patient__last_consultation__assigned_to=user) q_filters |= Q(patient__assigned_to=user) + q_filters |= Q(created_by=user) queryset = queryset.filter(q_filters) return queryset diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 5a30261099..ce07f7a7f3 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -16,7 +16,8 @@ class ExpectedPatientNoteKeys(Enum): CREATED_BY_OBJECT = "created_by_object" CREATED_DATE = "created_date" MODIFIED_DATE = "modified_date" - EDITS = "edits" + LAST_EDITED_BY = "last_edited_by" + LAST_EDITED_DATE = "last_edited_date" USER_TYPE = "user_type" @@ -121,7 +122,7 @@ def setUp(self): ) def create_patient_note( - self, patient=None, note="Patient is doing find", created_by=None, **kwargs + self, patient=None, note="Patient is doing fine", created_by=None, **kwargs ): data = { "facility": patient.facility or self.facility, @@ -132,6 +133,7 @@ def create_patient_note( self.client.post(f"/api/v1/patient/{patient.external_id}/notes/", data=data) def test_patient_notes(self): + self.client.force_authenticate(user=self.state_admin) patientId = self.patient.external_id response = self.client.get( f"/api/v1/patient/{patientId}/notes/?consultation={self.consultation.external_id}" @@ -221,25 +223,52 @@ def test_patient_notes(self): def test_patient_note_edit(self): patientId = self.patient.external_id - response = self.client.get(f"/api/v1/patient/{patientId}/notes/") + notes_list_response = self.client.get( + f"/api/v1/patient/{patientId}/notes/?consultation={self.consultation.external_id}" + ) + note_data = notes_list_response.json()["results"][0] + response = self.client.get( + f"/api/v1/patient/{patientId}/notes/{note_data['id']}/edits/" + ) - data = response.json()["results"][0] - self.assertEqual(len(data["edits"]), 1) + data = response.json()["results"] + self.assertEqual(len(data), 1) - note_id = data["id"] - note_content = data["note"] + note_content = note_data["note"] new_note_content = note_content + " edited" + # Test with a different user editing the note than the one who created it + self.client.force_authenticate(user=self.state_admin) + response = self.client.put( + f"/api/v1/patient/{patientId}/notes/{note_data['id']}/", + {"note": new_note_content}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.json()["Note"], "Only the user who created the note can edit it" + ) + + # Test with the same user editing the note + self.client.force_authenticate(user=self.user2) response = self.client.put( - f"/api/v1/patient/{patientId}/notes/{note_id}/", {"note": new_note_content} + f"/api/v1/patient/{patientId}/notes/{note_data['id']}/", + {"note": new_note_content}, ) + updated_data = response.json() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(updated_data["note"], new_note_content) - self.assertEqual(len(updated_data["edits"]), 2) - self.assertEqual(updated_data["edits"][0]["note"], new_note_content) - self.assertEqual(updated_data["edits"][1]["note"], note_content) + + # Ensure the original note is still present in the edits + response = self.client.get( + f"/api/v1/patient/{patientId}/notes/{note_data['id']}/edits/" + ) + + data = response.json()["results"] + self.assertEqual(len(data), 2) + self.assertEqual(data[0]["note"], new_note_content) + self.assertEqual(data[1]["note"], note_content) class PatientFilterTestCase(TestUtils, APITestCase):