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

Labs #2642

Draft
wants to merge 20 commits into
base: develop
Choose a base branch
from
Draft

Labs #2642

Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
332401e
added service request related valuesets, model and specs
khavinshankar Nov 28, 2024
3b388c3
added specimen related valuesets, model and specs
khavinshankar Nov 30, 2024
d4d57be
added signal to auto create specimen on creation of service request
khavinshankar Dec 4, 2024
5d2ae31
Merge branch 'vigneshhari/health-details' into khavinshankar/labs
khavinshankar Dec 8, 2024
a38f83d
resolved migration conflict and import path errors
khavinshankar Dec 8, 2024
ed50d6a
added diagnostic report related valuesets, model and specs
khavinshankar Dec 8, 2024
97a0fcd
added endpoints to facilitate the labs flow
khavinshankar Dec 9, 2024
96fbfd7
added status flow filter in specimen
khavinshankar Dec 12, 2024
9e57884
added verify and review apis in diagnostic report
khavinshankar Dec 15, 2024
6daf082
added phase filters for diagnostic report
khavinshankar Dec 15, 2024
9a831ca
Merge branch 'develop' into khavinshankar/labs
khavinshankar Jan 4, 2025
f46595c
fixed migration conflict
khavinshankar Jan 5, 2025
20514ae
updated the model references to emr models | created separate specs f…
khavinshankar Jan 5, 2025
8515c24
preserve default values if set manually
khavinshankar Jan 5, 2025
45cee9d
update condition type in specimen
khavinshankar Jan 5, 2025
ac0cbc4
fixed required field issues while using specs in other specs
khavinshankar Jan 5, 2025
86f215e
updated facility type filter to support multiple types
khavinshankar Jan 6, 2025
79db034
serialize subject, encounter and requester in service request list spec
khavinshankar Jan 6, 2025
a7e1bd0
Merge branch 'develop' into khavinshankar/labs
khavinshankar Jan 6, 2025
703d651
cleaned up migrations
khavinshankar Jan 6, 2025
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
57 changes: 57 additions & 0 deletions care/emr/api/viewsets/diagnostic_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework.decorators import action
from rest_framework.response import Response

from care.emr.api.viewsets.base import EMRModelViewSet
from care.emr.models.diagnostic_report import DiagnosticReport
from care.emr.models.observation import Observation
from care.emr.resources.diagnostic_report.spec import (
DiagnosticReportObservationRequest,
DiagnosticReportReadSpec,
DiagnosticReportSpec,
)
from care.emr.resources.observation.spec import Performer, PerformerType


@extend_schema_view(
create=extend_schema(request=DiagnosticReportSpec),
)
class DiagnosticReportViewSet(EMRModelViewSet):
database_model = DiagnosticReport
pydantic_model = DiagnosticReportSpec
pydantic_read_model = DiagnosticReportReadSpec

@extend_schema(
request=DiagnosticReportObservationRequest,
responses={200: DiagnosticReportReadSpec},
tags=["diagnostic_report"],
)
@action(detail=True, methods=["POST"])
def observations(self, request, *args, **kwargs):
data = DiagnosticReportObservationRequest(**request.data)
report: DiagnosticReport = self.get_object()

observations = []
for observation in data.observations:
if not observation.performer:
observation.performer = Performer(
type=PerformerType.user,
id=str(request.user.external_id),
)

observation_instance = observation.de_serialize()
observation_instance.subject_id = report.subject.id
observation_instance.encounter = report.encounter
observation_instance.patient = report.subject

observations.append(observation_instance)

observation_instances = Observation.objects.bulk_create(observations)
report.result.set(observation_instances)
report.save()

return Response(
self.get_read_pydantic_model()
.serialize(report)
.model_dump(exclude=["meta"]),
)
23 changes: 23 additions & 0 deletions care/emr/api/viewsets/service_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from drf_spectacular.utils import extend_schema, extend_schema_view

from care.emr.api.viewsets.base import EMRModelViewSet
from care.emr.models.service_request import ServiceRequest
from care.emr.resources.service_request.spec import (
ServiceRequestReadSpec,
ServiceRequestSpec,
)


@extend_schema_view(
create=extend_schema(request=ServiceRequestSpec),
)
class ServiceRequestViewSet(EMRModelViewSet):
database_model = ServiceRequest
pydantic_model = ServiceRequestSpec
pydantic_read_model = ServiceRequestReadSpec

def clean_create_data(self, request, *args, **kwargs):
clean_data = super().clean_create_data(request, *args, **kwargs)

clean_data["requester"] = request.user.external_id
return clean_data
142 changes: 142 additions & 0 deletions care/emr/api/viewsets/specimen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from datetime import UTC, datetime
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure Compatibility of datetime.now(UTC)

The use of UTC from the datetime module requires Python 3.11 or later. To maintain compatibility with earlier versions, consider importing timezone from datetime and using timezone.utc.

Apply this diff to ensure compatibility:

-from datetime import UTC, datetime
+from datetime import datetime, timezone

...

-    specimen.collected_at = datetime.now(UTC)
+    specimen.collected_at = datetime.now(timezone.utc)

...

-    specimen.dispatched_at = datetime.now(UTC)
+    specimen.dispatched_at = datetime.now(timezone.utc)

...

-    specimen.received_at = datetime.now(UTC)
+    specimen.received_at = datetime.now(timezone.utc)

...

-            note.time = DateTime(datetime.now(UTC).isoformat())
+            note.time = DateTime(datetime.now(timezone.utc).isoformat())

...

-                if not process.time:
-                    process.time = datetime.now(UTC)
+                if not process.time:
+                    process.time = datetime.now(timezone.utc)

Also applies to: 52-52, 74-74, 99-99, 103-103, 127-127


from django.db.models import Q
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response

from care.emr.api.viewsets.base import EMRModelViewSet
from care.emr.fhir.schema.base import DateTime
from care.emr.models.specimen import Specimen
from care.emr.resources.specimen.spec import (
SpecimenCollectRequest,
SpecimenProcessRequest,
SpecimenReadSpec,
SpecimenReceiveAtLabRequest,
SpecimenSendToLabRequest,
SpecimenSpec,
StatusChoices,
)


@extend_schema_view(
create=extend_schema(request=SpecimenSpec),
)
class SpecimenViewSet(EMRModelViewSet):
database_model = Specimen
pydantic_model = SpecimenSpec
pydantic_read_model = SpecimenReadSpec

def get_object(self) -> Specimen:
return get_object_or_404(
self.get_queryset(),
Q(external_id__iexact=self.kwargs[self.lookup_field])
| Q(identifier=self.kwargs[self.lookup_field])
| Q(accession_identifier=self.kwargs[self.lookup_field]),
)

@extend_schema(
request=SpecimenCollectRequest,
responses={200: SpecimenReadSpec},
tags=["specimen"],
)
@action(detail=True, methods=["POST"])
def collect(self, request, *args, **kwargs):
data = SpecimenCollectRequest(**request.data)
specimen = self.get_object()

specimen.identifier = data.identifier
specimen.status = StatusChoices.available
specimen.collected_at = datetime.now(UTC)
specimen.collected_by = request.user
specimen.save()

return Response(
self.get_read_pydantic_model()
.serialize(specimen)
.model_dump(exclude=["meta"]),
)

@extend_schema(
request=SpecimenSendToLabRequest,
responses={200: SpecimenReadSpec},
tags=["specimen"],
)
@action(detail=True, methods=["POST"])
def send_to_lab(self, request, *args, **kwargs):
data = SpecimenSendToLabRequest(**request.data)
specimen = self.get_object()
service_request = specimen.request

service_request.location = data.lab
specimen.dispatched_at = datetime.now(UTC)
specimen.dispatched_by = request.user
service_request.save()
specimen.save()

return Response(
self.get_read_pydantic_model()
.serialize(specimen)
.model_dump(exclude=["meta"]),
status=status.HTTP_200_OK,
)

@extend_schema(
request=SpecimenReceiveAtLabRequest,
responses={200: SpecimenReadSpec},
tags=["specimen"],
)
@action(detail=True, methods=["POST"])
def receive_at_lab(self, request, *args, **kwargs):
data = SpecimenReceiveAtLabRequest(**request.data)
specimen = self.get_object()
note = data.note

specimen.accession_identifier = data.accession_identifier
specimen.condition = data.condition
specimen.received_at = datetime.now(UTC)
specimen.received_by = request.user
if note:
note.authorReference = {"id": request.user.external_id}
note.time = DateTime(datetime.now(UTC).isoformat())
specimen.note.append(note.model_dump(mode="json"))
specimen.save()

return Response(
self.get_read_pydantic_model()
.serialize(specimen)
.model_dump(exclude=["meta"]),
status=status.HTTP_200_OK,
)

@extend_schema(
request=SpecimenProcessRequest,
responses={200: SpecimenReadSpec},
tags=["specimen"],
)
@action(detail=True, methods=["POST"])
def process(self, request, *args, **kwargs):
data = SpecimenProcessRequest(**request.data)
specimen = self.get_object()

processes = []
for process in data.process:
if not process.time:
process.time = datetime.now(UTC)

if not process.performer:
process.performer = request.user.external_id

processes.append(process.model_dump(mode="json"))

specimen.processing.extend(processes)
specimen.save()

return Response(
self.get_read_pydantic_model()
.serialize(specimen)
.model_dump(exclude=["meta"]),
status=status.HTTP_200_OK,
)
3 changes: 3 additions & 0 deletions care/emr/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
class EMRConfig(AppConfig):
name = "care.emr"
verbose_name = _("Electronic Medical Record")

def ready(self):
import care.emr.signals # noqa
91 changes: 91 additions & 0 deletions care/emr/fhir/resources/concept_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from enum import Enum

from pydantic.main import BaseModel

from care.emr.fhir.resources.base import ResourceManger
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Typo: ResourceManger should be ResourceManager

It seems that ResourceManger is misspelled in both the import statement and the class definition. Correcting this will prevent potential ImportErrors and ensure consistency.

Apply this diff to fix the typo:

-from care.emr.fhir.resources.base import ResourceManger
+from care.emr.fhir.resources.base import ResourceManager

...

-class ConceptMapResource(ResourceManger):
+class ConceptMapResource(ResourceManager):

Also applies to: 9-9

from care.emr.fhir.utils import parse_fhir_parameter_output


class ConceptMapResource(ResourceManger):
allowed_properties = ["system", "code"]
resource = "ConceptMap"

def serialize_lookup(self, result):
structured_output = parse_fhir_parameter_output(result)

return ConceptMapResult(
result=structured_output["result"],
match=[
ConceptMapMatch(
equivalence=match["equivalence"],
concept=ConceptMapConcept(
display=match["concept"]["display"],
code=match["concept"]["code"],
),
source=match["source"],
)
for match in structured_output["match"]
],
)

def translate(self):
if "system" not in self._filters or "code" not in self._filters:
err = "Both system and code are required"
raise ValueError(err)
full_result = self.query("GET", "ConceptMap/$translate", self._filters)

return self.serialize_lookup(full_result["parameter"])


class ConceptMapConcept(BaseModel):
display: str
code: str


class ConceptMapEquivalence(str, Enum):
def __new__(cls, value, priority=None):
obj = str.__new__(cls, value)
obj._value_ = value
obj.priority = priority
return obj

# This is the strongest form of equality.
equal = "equal", 1

# Indicates equivalence, almost identical.
equivalent = "equivalent", 2

# Concepts are related, exact nature unspecified.
relatedto = "relatedto", 3

# The source concept is more specific than the target.
specializes = "specializes", 4

# The source concept is more general and subsumes the target.
subsumes = "subsumes", 5

# The source is broader in meaning than the target.
wider = "wider", 6

# The source is narrower in meaning than the target.
narrower = "narrower", 7

# The relationship is approximate but not exact.
inexact = "inexact", 8

# Indicates a complete lack of relationship or overlap.
disjoint = "disjoint", 9

# No match exists for the source concept in the target system.
unmatched = "unmatched", 10


class ConceptMapMatch(BaseModel):
equivalence: ConceptMapEquivalence
concept: ConceptMapConcept
source: str


class ConceptMapResult(BaseModel):
result: bool
match: list[ConceptMapMatch]
14 changes: 14 additions & 0 deletions care/emr/fhir/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,26 @@ def parse_fhir_parameter_output(parameters):
response = {}
for parameter in parameters:
value = ""

if "valueString" in parameter:
value = parameter["valueString"]
if "valueBoolean" in parameter:
value = parameter["valueBoolean"]
if "valueCode" in parameter:
value = parameter["valueCode"]
if "valueCoding" in parameter:
value = parameter["valueCoding"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

How trusting of our input data

While adding support for new value types is great, we might want to add some validation. You know, just in case someone sends us something... unexpected.

Consider adding type validation and error handling:

 if "valueBoolean" in parameter:
+    if not isinstance(parameter["valueBoolean"], bool):
+        raise ValueError(f"Expected boolean value, got {type(parameter['valueBoolean'])}")
     value = parameter["valueBoolean"]

Committable suggestion skipped: line range outside the PR's diff.


if parameter["name"] == "property":
if "property" not in response:
response["property"] = {}
response["property"].update(parse_fhir_property_part(parameter["part"]))
elif parameter["name"] == "match":
if "match" not in response:
response["match"] = []

response["match"].append(parse_fhir_parameter_output(parameter["part"]))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ah, infinite recursion, my old friend

The recursive call for handling "match" parameters could potentially lead to a stack overflow with deeply nested structures. Not that anyone would do that intentionally, right?

Consider adding a depth limit:

-def parse_fhir_parameter_output(parameters):
+def parse_fhir_parameter_output(parameters, depth=0, max_depth=10):
+    if depth >= max_depth:
+        raise ValueError("Maximum nesting depth exceeded")
     response = {}
     for parameter in parameters:
         # ... existing code ...
         elif parameter["name"] == "match":
             if "match" not in response:
                 response["match"] = []
-            response["match"].append(parse_fhir_parameter_output(parameter["part"]))
+            response["match"].append(parse_fhir_parameter_output(parameter["part"], depth + 1))

Committable suggestion skipped: line range outside the PR's diff.

else:
response[parameter["name"]] = value

return response
Loading
Loading