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

Merge Develop To Staging v3.1.0 #2814

Merged
merged 18 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading