Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(patient notes): add edit window validation and update endpoint #1221

Merged
merged 45 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e378637
feat(patient notes): add edit window validation and update endpoint
Ashesh3 Mar 18, 2023
0e5ef10
Merge branch 'master' into doc-note-edit
Ashesh3 Apr 18, 2023
b68cdb4
Use editable_until logic
Ashesh3 Apr 18, 2023
d6a3574
dummy empty commit
Ashesh3 Apr 18, 2023
aa780a5
Merge branch 'master' into doc-note-edit
Ashesh3 Jul 19, 2023
bcf1ffa
Merge branch 'master' into doc-note-edit
Ashesh3 Jul 19, 2023
18dc937
Merge migrations
Ashesh3 Jul 19, 2023
11b91df
format files
Ashesh3 Jul 19, 2023
3d85cc4
Merge migrations
Ashesh3 Jul 19, 2023
85531fe
fix tests
Ashesh3 Jul 19, 2023
aec33b5
remove editable_until
Ashesh3 Jul 21, 2023
38b50b1
Merge branch 'master' into doc-note-edit
Ashesh3 Jul 21, 2023
551d487
bug fixes
Ashesh3 Jul 21, 2023
548b11e
Move update validation to viewset
Ashesh3 Jul 21, 2023
ecfef45
Merge branch 'master' into doc-note-edit
Ashesh3 Jul 21, 2023
c415185
Fix tests
Ashesh3 Jul 21, 2023
4dd8d12
Remove PATIENT_NOTE_EDIT_WINDOW
Ashesh3 Aug 8, 2023
8e61d90
Merge branch 'master' into doc-note-edit
Ashesh3 Aug 8, 2023
2e90292
fix lint
Ashesh3 Aug 8, 2023
44d32f0
Merge branch 'master' into doc-note-edit
Ashesh3 Aug 28, 2023
067d147
edit history for patient notes
Ashesh3 Aug 28, 2023
4eaab8b
Fix tests
Ashesh3 Aug 29, 2023
3587a87
Create initial edit record for existing notes
Ashesh3 Aug 29, 2023
dc899d0
optimize migration
sainak Aug 29, 2023
7e44d7a
Update care/facility/models/mixins/permissions/patient.py
Ashesh3 Aug 29, 2023
5adcd17
Merge branch 'master' into doc-note-edit
Ashesh3 Dec 20, 2023
f291055
Update migrations
Ashesh3 Dec 20, 2023
c42e923
Merge branch 'master' into doc-note-edit
Ashesh3 Dec 27, 2023
0117da4
update migrations
Ashesh3 Dec 27, 2023
2d9bf40
Update care/facility/api/serializers/patient.py
Ashesh3 Dec 27, 2023
d8050c2
Update care/facility/models/patient.py
Ashesh3 Dec 27, 2023
1e50d34
Update care/facility/migrations/0405_patientnotesedit.py
sainak Dec 27, 2023
c7d09ea
Update care/facility/migrations/0405_patientnotesedit.py
sainak Dec 27, 2023
c61d3e9
edited_date
Ashesh3 Dec 27, 2023
ef69e7e
Update care/facility/migrations/0405_patientnotesedit.py
sainak Dec 27, 2023
559d50b
Merge branch 'doc-note-edit' of https://github.com/ashesh3/care into …
Ashesh3 Dec 27, 2023
a54e2c3
Update care/facility/models/patient.py
sainak Dec 27, 2023
eaf8f68
Apply suggestions from code review
sainak Dec 27, 2023
26cc98d
Merge branch 'master' into doc-note-edit
Ashesh3 Jan 2, 2024
73750d7
Merge branch 'master' into doc-note-edit
Ashesh3 Jan 9, 2024
d3bcb79
Merge branch 'master' into doc-note-edit
Ashesh3 Jan 17, 2024
59a7b0e
Fix N+1, update migrations, seperate endpoint for edits
Ashesh3 Jan 18, 2024
6f9039c
Merge branch 'master' into doc-note-edit
Ashesh3 Jan 22, 2024
716368e
Update migrations
Ashesh3 Jan 22, 2024
3ecf5b6
Fix tests
Ashesh3 Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
# 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

Check warning on line 537 in care/facility/api/serializers/patient.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/patient.py#L537

Added line #L537 was not covered by tests

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",
Ashesh3 marked this conversation as resolved.
Show resolved Hide resolved
"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 @@ -49,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,
Expand Down Expand Up @@ -633,25 +639,79 @@
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

Check warning on line 660 in care/facility/api/viewsets/patient.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/patient.py#L660

Added line #L660 was not covered by tests
if user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]:
queryset = queryset.filter(

Check warning on line 662 in care/facility/api/viewsets/patient.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/patient.py#L662

Added line #L662 was not covered by tests
patient_note__patient__facility__state=user.state
)
elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]:
queryset = queryset.filter(

Check warning on line 666 in care/facility/api/viewsets/patient.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/patient.py#L666

Added line #L666 was not covered by tests
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 @@ -662,6 +722,7 @@
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 @@ -674,7 +735,7 @@
)
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 @@ -708,3 +769,23 @@
).generate()

return instance

def perform_update(self, serializer):
Ashesh3 marked this conversation as resolved.
Show resolved Hide resolved
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(

Check warning on line 782 in care/facility/api/viewsets/patient.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/patient.py#L782

Added line #L782 was not covered by tests
{"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 @@ -734,3 +734,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