-
Notifications
You must be signed in to change notification settings - Fork 331
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
base: develop
Are you sure you want to change the base?
Labs #2642
Changes from 7 commits
332401e
3b388c3
d4d57be
5d2ae31
a38f83d
ed50d6a
97a0fcd
96fbfd7
9e57884
6daf082
9a831ca
f46595c
20514ae
8515c24
45cee9d
ac0cbc4
86f215e
79db034
a7e1bd0
703d651
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"]), | ||
) |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
from datetime import UTC, datetime | ||
|
||
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, | ||
) |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: It seems that 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] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"]
|
||
|
||
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"])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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))
|
||
else: | ||
response[parameter["name"]] = value | ||
|
||
return response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure Compatibility of
datetime.now(UTC)
The use of
UTC
from thedatetime
module requires Python 3.11 or later. To maintain compatibility with earlier versions, consider importingtimezone
fromdatetime
and usingtimezone.utc
.Apply this diff to ensure compatibility:
Also applies to: 52-52, 74-74, 99-99, 103-103, 127-127