Skip to content

Commit

Permalink
feat(patient notes): add edit window validation and update endpoint (#…
Browse files Browse the repository at this point in the history
…1221)

* 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

* Use editable_until logic

* dummy empty commit

* Merge migrations

* format files

* Merge migrations

* fix tests

* remove editable_until

* bug fixes

* Move update validation to viewset

* Fix tests

* Remove PATIENT_NOTE_EDIT_WINDOW

* fix lint

* edit history for patient notes

* Fix tests

* Create initial edit record for existing notes

* optimize migration

Signed-off-by: Aakash Singh <mail@singhaakash.dev>

* Update care/facility/models/mixins/permissions/patient.py

Co-authored-by: Aakash Singh <mail@singhaakash.dev>

* Update migrations

* update migrations

* Update care/facility/api/serializers/patient.py

Co-authored-by: Aakash Singh <mail@singhaakash.dev>

* Update care/facility/models/patient.py

Co-authored-by: Aakash Singh <mail@singhaakash.dev>

* Update care/facility/migrations/0405_patientnotesedit.py

* Update care/facility/migrations/0405_patientnotesedit.py

* edited_date

* Update care/facility/migrations/0405_patientnotesedit.py

* Update care/facility/models/patient.py

* Apply suggestions from code review

* Fix N+1, update migrations, seperate endpoint for edits

* Update migrations

* Fix tests

---------

Signed-off-by: Aakash Singh <mail@singhaakash.dev>
Co-authored-by: Aakash Singh <mail@singhaakash.dev>
  • Loading branch information
Ashesh3 and sainak authored Feb 12, 2024
1 parent eeed4fd commit e623b74
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 9 deletions.
56 changes: 54 additions & 2 deletions care/facility/api/serializers/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)
from care.facility.models.bed import ConsultationBed
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,
Expand Down Expand Up @@ -475,9 +476,20 @@ def update(self, instance, validated_data):
return instance


class PatientNotesEditSerializer(serializers.ModelSerializer):
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)
last_edited_by = serializers.CharField(read_only=True)
last_edited_date = serializers.DateTimeField(read_only=True)
consultation = ExternalIdSerializerField(
queryset=PatientConsultation.objects.all(),
required=False,
Expand All @@ -503,16 +515,56 @@ 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_date=instance.modified_date,
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():
instance = super().update(instance, validated_data)
edit = PatientNotesEdit(
patient_note=instance,
edited_date=instance.modified_date,
edited_by=user,
note=note,
)
edit.save()
return instance

class Meta:
model = PatientNotes
fields = (
"id",
"note",
"facility",
"consultation",
"created_by_object",
"user_type",
"created_date",
"modified_date",
"last_edited_by",
"last_edited_date",
)
read_only_fields = (
"id",
"created_date",
"modified_date",
"last_edited_by",
"last_edited_date",
)
read_only_fields = ("created_date",)
93 changes: 87 additions & 6 deletions care/facility/api/viewsets/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,7 +16,12 @@
from rest_framework.exceptions import ValidationError
from rest_framework.filters import BaseFilterBackend
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
Expand All @@ -27,6 +31,7 @@
FacilityPatientStatsHistorySerializer,
PatientDetailSerializer,
PatientListSerializer,
PatientNotesEditSerializer,
PatientNotesSerializer,
PatientSearchSerializer,
PatientTransferSerializer,
Expand All @@ -53,6 +58,7 @@
ConditionVerificationStatus,
)
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,
Expand Down Expand Up @@ -668,25 +674,79 @@ 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_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


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")
)
lookup_field = "external_id"
serializer_class = PatientNotesSerializer
permission_classes = (IsAuthenticated, DRYPermissions)
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = PatientNotesFilterSet

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)
Expand All @@ -697,6 +757,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
Expand All @@ -709,7 +770,7 @@ 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"}
)

instance = serializer.save(
Expand Down Expand Up @@ -743,3 +804,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)
76 changes: 76 additions & 0 deletions care/facility/migrations/0408_patientnotesedit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Generated by Django 4.2.2 on 2023-08-28 14:09

import django.db.models.deletion
from django.conf import settings
from django.core.paginator import Paginator
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")

notes_without_edits = PatientNotes.objects.all()

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_date=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),
("facility", "0407_alter_dailyround_additional_symptoms_and_more"),
]

operations = [
migrations.CreateModel(
name="PatientNotesEdit",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("edited_date", 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_date"],
},
),
migrations.RunPython(
code=create_initial_patient_notes_edit_record,
reverse_code=migrations.RunPython.noop,
),
]
19 changes: 19 additions & 0 deletions care/facility/models/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,22 @@ def get_related_consultation(self):
# and hence the permission mixin will fail if edit/object_read permissions are checked (although not used as of now)
# Remove once patient notes is made consultation specific.
return self


class PatientNotesEdit(models.Model):
patient_note = models.ForeignKey(
PatientNotes,
on_delete=models.CASCADE,
null=False,
blank=False,
related_name="edits",
)
edited_date = 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_date"]
Loading

0 comments on commit e623b74

Please sign in to comment.