Skip to content

Commit

Permalink
Merge pull request #2814 from ohcnetwork/develop
Browse files Browse the repository at this point in the history
Merge Develop To Staging v3.1.0
  • Loading branch information
khavinshankar authored Feb 7, 2025
2 parents 4e4731a + 6b34820 commit 9828d3f
Show file tree
Hide file tree
Showing 30 changed files with 722 additions and 371 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ migrate:

test:
docker compose exec backend bash -c "python manage.py test $(path) --keepdb --parallel --shuffle"

test-no-keep:
docker compose exec backend bash -c "python manage.py test $(path) --parallel --shuffle"


test-coverage:
docker compose exec backend bash -c "coverage run manage.py test --settings=config.settings.test --keepdb --parallel --shuffle"
docker compose exec backend bash -c "coverage combine || true; coverage xml"
Expand Down
10 changes: 5 additions & 5 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name = "pypi"
[packages]
argon2-cffi = "==23.1.0"
authlib = "==1.4.0"
boto3 = "==1.36.7"
boto3 = "==1.36.12"
celery = "==5.4.0"
django = "==5.1.4"
django-environ = "==0.12.0"
Expand All @@ -23,7 +23,7 @@ djangorestframework = "==3.15.2"
djangorestframework-simplejwt = "==5.4.0"
dry-rest-permissions = "==0.1.10"
drf-nested-routers = "==0.94.1"
drf-spectacular = "==0.27.2"
drf-spectacular = "==0.28.0"
gunicorn = "==23.0.0"
healthy-django = "==0.1.0"
json-fingerprint = "==0.14.0"
Expand All @@ -43,17 +43,17 @@ sentry-sdk = "==2.18.0"
whitenoise = "==6.8.2"
django-anymail = {extras = ["amazon-ses"], version = "*"}
pydantic-extra-types = "2.10.2"
phonenumberslite = "==8.13.52"
phonenumberslite = "==8.13.54"

[dev-packages]
boto3-stubs = { extras = ["s3", "boto3"], version = "*" }
coverage = "==7.6.10"
debugpy = "==1.8.11"
debugpy = "==1.8.12"
django-coverage-plugin = "==3.1.0"
django-extensions = "==3.2.3"
django-silk = "==5.3.2"
djangorestframework-stubs = "==3.15.2"
factory-boy = "==3.3.1"
factory-boy = "==3.3.3"
freezegun = "==1.5.1"
ipython = "==8.31.0"
mypy = "==1.14.1"
Expand Down
168 changes: 88 additions & 80 deletions Pipfile.lock

Large diffs are not rendered by default.

43 changes: 38 additions & 5 deletions care/emr/api/viewsets/allergy_intolerance.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import uuid

from django.db import transaction
from django_filters import CharFilter, FilterSet
from django_filters import rest_framework as filters
from django_filters.rest_framework import DjangoFilterBackend
Expand Down Expand Up @@ -68,6 +71,22 @@ def authorize_create(self, instance):
):
raise PermissionDenied("You do not have permission to update encounter")

def perform_update(self, instance):
"""
Updates need to check if the encounter of the instance has been changed, If, so a copy object needs to be created.
"""
database_copy = AllergyIntolerance.objects.get(id=instance.id)
with transaction.atomic():
if database_copy.encounter != instance.encounter:
database_copy.copied_from = database_copy.id
database_copy.id = None
database_copy.external_id = uuid.uuid4()
database_copy.save()
AllergyIntolerance.objects.filter(
encounter=instance.encounter, copied_from=instance.id
).delete()
return super().perform_update(instance)

def authorize_update(self, request_obj, model_instance):
encounter = get_object_or_404(Encounter, external_id=request_obj.encounter)
if not AuthorizationController.call(
Expand All @@ -81,17 +100,31 @@ def clean_update_data(self, request_data):
return super().clean_update_data(request_data, keep_fields={"encounter"})

def get_queryset(self):
if not AuthorizationController.call(
"can_view_clinical_data", self.request.user, self.get_patient_obj()
):
raise PermissionDenied("Permission denied for patient data")
return (
queryset = (
super()
.get_queryset()
.filter(patient__external_id=self.kwargs["patient_external_id"])
.select_related("patient", "encounter", "created_by", "updated_by")
.order_by("-modified_date")
)

if not AuthorizationController.call(
"can_view_clinical_data", self.request.user, self.get_patient_obj()
):
encounter = Encounter.objects.filter(
external_id=self.request.GET.get("encounter")
).first()

# Check for encounter access
if not encounter or not AuthorizationController.call(
"can_view_encounter_obj", self.request.user, encounter
):
raise PermissionDenied("Permission denied to user")
queryset = queryset.filter(encounter=encounter)

else:
queryset = queryset.filter(copied_from__isnull=True)
return queryset


InternalQuestionnaireRegistry.register(AllergyIntoleranceViewSet)
Empty file.
8 changes: 8 additions & 0 deletions care/emr/api/viewsets/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
class FacilityLocationFilter(filters.FilterSet):
parent = filters.UUIDFilter(field_name="parent__external_id")
name = filters.CharFilter(field_name="name", lookup_expr="icontains")
mode = filters.CharFilter(field_name="mode", lookup_expr="iexact")
availability_status = filters.CharFilter(
field_name="availability_status", lookup_expr="iexact"
)
operational_status = filters.CharFilter(
field_name="operational_status", lookup_expr="iexact"
)
status = filters.CharFilter(field_name="status", lookup_expr="iexact")


class FacilityLocationViewSet(EMRModelViewSet):
Expand Down
2 changes: 2 additions & 0 deletions care/emr/api/viewsets/medication_administration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from care.emr.resources.medication.administration.spec import (
MedicationAdministrationReadSpec,
MedicationAdministrationSpec,
MedicationAdministrationUpdateSpec,
)
from care.emr.resources.questionnaire.spec import SubjectType

Expand All @@ -25,6 +26,7 @@ class MedicationAdministrationViewSet(
):
database_model = MedicationAdministration
pydantic_model = MedicationAdministrationSpec
pydantic_update_model = MedicationAdministrationUpdateSpec
pydantic_read_model = MedicationAdministrationReadSpec
questionnaire_type = "medication_administration"
questionnaire_title = "Medication Administration"
Expand Down
1 change: 1 addition & 0 deletions care/emr/api/viewsets/questionnaire.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class QuestionnaireFilter(filters.FilterSet):
title = filters.CharFilter(field_name="title", lookup_expr="icontains")
subject_type = filters.CharFilter(field_name="subject_type", lookup_expr="iexact")
tag_slug = QuestionnaireTagSlugFilter(field_name="tag_slug")
status = filters.CharFilter(field_name="status", lookup_expr="iexact")


class QuestionnaireViewSet(EMRModelViewSet):
Expand Down
8 changes: 7 additions & 1 deletion care/emr/api/viewsets/scheduling/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from care.emr.models.scheduling.booking import TokenSlot
from care.emr.models.scheduling.schedule import Availability, Schedule
from care.emr.resources.scheduling.schedule.spec import (
AvailabilityCreateSpec,
AvailabilityForScheduleSpec,
ScheduleCreateSpec,
ScheduleReadSpec,
Expand Down Expand Up @@ -132,7 +133,8 @@ def get_queryset(self):

class AvailabilityViewSet(EMRCreateMixin, EMRDestroyMixin, EMRBaseViewSet):
database_model = Availability
pydantic_model = AvailabilityForScheduleSpec
pydantic_model = AvailabilityCreateSpec
pydantic_retrieve_model = AvailabilityForScheduleSpec

def get_facility_obj(self):
return get_object_or_404(
Expand Down Expand Up @@ -164,6 +166,10 @@ def get_queryset(self):
.order_by("-modified_date")
)

def clean_create_data(self, request_data):
request_data["schedule"] = self.kwargs["schedule_external_id"]
return request_data

def perform_create(self, instance):
schedule = self.get_schedule_obj()
instance.schedule = schedule
Expand Down
5 changes: 3 additions & 2 deletions care/emr/api/viewsets/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ def perform_create(self, instance):

def authorize_update(self, request_obj, model_instance):
if self.request.user.is_superuser:
return True
return self.request.user.id == model_instance.id
return
if not self.request.user.id == model_instance.id:
raise PermissionDenied("You do not have permission to update this user")

def authorize_create(self, instance):
if not AuthorizationController.call("can_create_user", self.request.user):
Expand Down
18 changes: 18 additions & 0 deletions care/emr/migrations/0016_allergyintolerance_copied_from.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-02-04 17:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('emr', '0015_facilitylocation_availability_status_and_more'),
]

operations = [
migrations.AddField(
model_name='allergyintolerance',
name='copied_from',
field=models.BigIntegerField(blank=True, default=None, null=True),
),
]
3 changes: 3 additions & 0 deletions care/emr/models/allergy_intolerance.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ class AllergyIntolerance(EMRBaseModel):
recorded_date = models.DateTimeField(null=True, blank=True)
last_occurrence = models.DateTimeField(null=True, blank=True)
note = models.TextField(null=True, blank=True)
copied_from = models.BigIntegerField(
default=None, null=True, blank=True
) # If True, the record is a copy maintained of the given ID
1 change: 1 addition & 0 deletions care/emr/resources/allergy_intolerance/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class AllergyIntoleranceReadSpec(BaseAllergyIntoleranceSpec):
recorded_date: datetime.datetime | None = None
created_by: dict = {}
updated_by: dict = {}
note: str | None = None

@classmethod
def perform_extra_serialization(cls, mapping, obj):
Expand Down
2 changes: 1 addition & 1 deletion care/emr/resources/encounter/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class DischargeDispositionChoices(str, Enum):

class DietPreferenceChoices(str, Enum):
vegetarian = "vegetarian"
diary_free = "diary_free"
dairy_free = "dairy_free"
nut_free = "nut_free"
gluten_free = "gluten_free"
vegan = "vegan"
Expand Down
25 changes: 18 additions & 7 deletions care/emr/resources/encounter/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from django.utils import timezone
from pydantic import UUID4, BaseModel, model_validator

from care.emr.models import Encounter, EncounterOrganization, TokenBooking
from care.emr.models import (
Encounter,
EncounterOrganization,
FacilityLocationEncounter,
TokenBooking,
)
from care.emr.models.patient import Patient
from care.emr.resources.base import EMRResource
from care.emr.resources.encounter.constants import (
Expand All @@ -17,10 +22,12 @@
)
from care.emr.resources.facility.spec import FacilityBareMinimumSpec
from care.emr.resources.facility_organization.spec import FacilityOrganizationReadSpec
from care.emr.resources.location.spec import FacilityLocationListSpec
from care.emr.resources.location.spec import (
FacilityLocationEncounterListSpecWithLocation,
FacilityLocationListSpec,
)
from care.emr.resources.patient.spec import PatientListSpec
from care.emr.resources.scheduling.slot.spec import TokenBookingReadSpec
from care.emr.resources.user.spec import UserSpec
from care.facility.models import Facility


Expand Down Expand Up @@ -118,6 +125,7 @@ class EncounterRetrieveSpec(EncounterListSpec):
updated_by: dict = {}
organizations: list[dict] = []
current_location: dict | None = None
location_history: list[dict] = []

@classmethod
def perform_extra_serialization(cls, mapping, obj):
Expand All @@ -136,7 +144,10 @@ def perform_extra_serialization(cls, mapping, obj):
mapping["current_location"] = FacilityLocationListSpec.serialize(
obj.current_location
).to_json()
if obj.created_by:
mapping["created_by"] = UserSpec.serialize(obj.created_by)
if obj.updated_by:
mapping["updated_by"] = UserSpec.serialize(obj.updated_by)
mapping["location_history"] = [
FacilityLocationEncounterListSpecWithLocation.serialize(i)
for i in FacilityLocationEncounter.objects.filter(encounter=obj).order_by(
"-created_date"
)
]
cls.serialize_audit_users(mapping, obj)
20 changes: 20 additions & 0 deletions care/emr/resources/location/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@ class FacilityLocationEncounterUpdateSpec(FacilityLocationEncounterBaseSpec):
end_datetime: datetime.datetime | None = None


class FacilityLocationEncounterListSpec(FacilityLocationEncounterBaseSpec):
encounter: UUID4
start_datetime: datetime.datetime
end_datetime: datetime.datetime | None = None
status: str

@classmethod
def perform_extra_serialization(cls, mapping, obj):
mapping["id"] = obj.external_id


class FacilityLocationEncounterListSpecWithLocation(FacilityLocationEncounterListSpec):
location: dict

@classmethod
def perform_extra_serialization(cls, mapping, obj):
super().perform_extra_serialization(mapping, obj)
mapping["location"] = FacilityLocationListSpec.serialize(obj.location).to_json()


class FacilityLocationEncounterReadSpec(FacilityLocationEncounterBaseSpec):
encounter: UUID4
start_datetime: datetime.datetime
Expand Down
20 changes: 12 additions & 8 deletions care/emr/resources/medication/administration/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,8 @@ class BaseMedicationAdministrationSpec(EMRResource):
occurrence_period_start: datetime = Field(
description="When the medication was administration started",
)
occurrence_period_end: datetime | None = Field(
None,
description="When the medication administration ended. If not provided, it is assumed to be ongoing",
)
occurrence_period_end: datetime | None = None

recorded: datetime | None = Field(
None,
description="When administration was recorded",
Expand All @@ -167,10 +165,7 @@ class BaseMedicationAdministrationSpec(EMRResource):
description="The dosage of the medication",
)

note: str | None = Field(
None,
description="Any additional notes about the medication",
)
note: str | None = None


class MedicationAdministrationSpec(BaseMedicationAdministrationSpec):
Expand Down Expand Up @@ -217,6 +212,15 @@ def perform_extra_deserialization(self, is_update, obj):
obj.request = MedicationRequest.objects.get(external_id=self.request)


class MedicationAdministrationUpdateSpec(EMRResource):
__model__ = MedicationAdministration
__exclude__ = ["patient", "encounter", "request"]

status: MedicationAdministrationStatus
note: str | None = None
occurrence_period_end: datetime | None = None


class MedicationAdministrationReadSpec(BaseMedicationAdministrationSpec):
created_by: UserSpec = dict

Expand Down
19 changes: 10 additions & 9 deletions care/emr/resources/questionnaire/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,21 +225,22 @@ def create_observation_spec(questionnaire, responses, parent_id=None):
for value in responses[questionnaire["id"]].values:
observation = spec.copy()
observation["id"] = str(uuid.uuid4())
if questionnaire["type"] == QuestionType.choice.value and value.value_code:
observation["value"] = {
"value_code": (value.value_code.model_dump(exclude_defaults=True))
}
if questionnaire["type"] == QuestionType.choice.value and value.code:
observation["value"] = value.value_code.model_dump(
exclude_defaults=True
)

elif (
questionnaire["type"] == QuestionType.quantity.value
and value.value_quantity
):
observation["value"] = {
"value_quantity": (
value.value_quantity.model_dump(exclude_defaults=True)
)
}
observation["value"] = value.value_quantity.model_dump(
exclude_defaults=True
)
elif value:
observation["value"] = {"value": value.value}
if "unit" in questionnaire:
observation["value"]["unit"] = questionnaire["unit"]
if responses[questionnaire["id"]].note:
observation["note"] = responses[questionnaire["id"]].note
if parent_id:
Expand Down
Loading

0 comments on commit 9828d3f

Please sign in to comment.