From e054efda8addaf6c60e9760a0cd79841fca6d998 Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Thu, 24 Oct 2024 12:55:59 -0500 Subject: [PATCH 1/8] Added ProtocolOverride to data module --- canvas_sdk/v1/data/protocol_override.py | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 canvas_sdk/v1/data/protocol_override.py diff --git a/canvas_sdk/v1/data/protocol_override.py b/canvas_sdk/v1/data/protocol_override.py new file mode 100644 index 00000000..70a21a84 --- /dev/null +++ b/canvas_sdk/v1/data/protocol_override.py @@ -0,0 +1,59 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from canvas_sdk.v1.data.base import CommittableModelManager +from canvas_sdk.v1.data.patient import Patient +from canvas_sdk.v1.data.user import CanvasUser + + +class IntervalUnit(models.TextChoices): + """ProtocolOverride cycle IntervalUnit.""" + + DAYS = "days", _("days") + MONTHS = "months", _("months") + YEARS = "years", _("years") + + +class Status(models.TextChoices): + """ProtocolOverride Status.""" + + ACTIVE = "active", _("active") + INACTIVE = "inactive", _("inactive") + + +class ProtocolOverride(models.Model): + """ProtocolOverride.""" + + class Meta: + managed = False + app_label = "canvas_sdk" + db_table = "canvas_sdk_data_api_protocoloverride_001" + + objects = CommittableModelManager() + + id = models.UUIDField() + dbid = models.BigIntegerField(primary_key=True) + created = models.DateTimeField() + modified = models.DateTimeField() + deleted = models.BooleanField() + committer = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) + entered_in_error = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) + patient = models.ForeignKey( + Patient, + on_delete=models.DO_NOTHING, + related_name="allergy_intolerances", + ) + protocol_key = models.CharField() + is_adjustment = models.BooleanField() + reference_date = models.DateTimeField() + cycle_in_days = models.IntegerField() + is_snooze = models.BooleanField() + snooze_date = models.DateField() + snoozed_days = models.IntegerField() + # reason_id = models.BigIntegerField() + snooze_comment = models.TextField() + narrative = models.CharField() + # note_id = models.BigIntegerField() + cycle_quantity = models.IntegerField() + cycle_unit = models.CharField(choices=IntervalUnit.choices) + status = models.CharField(choices=Status.choices) From 805445973697ee951b0c0d20a9537d6ac08cd7fb Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Thu, 31 Oct 2024 10:32:34 -0500 Subject: [PATCH 2/8] Refactored how ValueSet queries are determined --- canvas_sdk/v1/data/questionnaire.py | 14 +----------- canvas_sdk/value_set/v2022/assessment.py | 27 +++++++++++++++++------- canvas_sdk/value_set/value_set.py | 14 +++++++++++- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/canvas_sdk/v1/data/questionnaire.py b/canvas_sdk/v1/data/questionnaire.py index a2215b0a..1bd8f70d 100644 --- a/canvas_sdk/v1/data/questionnaire.py +++ b/canvas_sdk/v1/data/questionnaire.py @@ -1,7 +1,4 @@ -from collections.abc import Container - from django.db import models -from django.db.models import Q from canvas_sdk.v1.data import Patient from canvas_sdk.v1.data.base import ( @@ -75,15 +72,6 @@ class Meta: code = models.CharField() -class QuestionnaireValueSetLookupQuerySet(ValueSetLookupByNameQuerySet): - """QuerySet class for Questionaire ValueSet lookups.""" - - @staticmethod - def q_object(system: str, codes: Container[str]) -> Q: - """The code system and code values for a Questionnaire are just attributes on the model.""" - return Q(code_system=system, code__in=codes) - - class Questionnaire(models.Model): """Questionnaire.""" @@ -92,7 +80,7 @@ class Meta: app_label = "canvas_sdk" db_table = "canvas_sdk_data_api_questionnaire_001" - objects = models.Manager.from_queryset(QuestionnaireValueSetLookupQuerySet)() + objects = models.Manager.from_queryset(ValueSetLookupByNameQuerySet)() id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/value_set/v2022/assessment.py b/canvas_sdk/value_set/v2022/assessment.py index 0c0eb54e..bf6dce1b 100644 --- a/canvas_sdk/value_set/v2022/assessment.py +++ b/canvas_sdk/value_set/v2022/assessment.py @@ -1,7 +1,18 @@ +from typing import Container + +from django.db.models import Q + from ..value_set import ValueSet -class TobaccoUseScreening(ValueSet): +class AssessmentValueSet(ValueSet): + @staticmethod + def q_object(system: str, codes: Container[str]) -> Q: + """The code system and code values for a Questionnaire are just attributes on the model.""" + return Q(code_system=system, code__in=codes) + + +class TobaccoUseScreening(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments to screen for tobacco use. @@ -27,7 +38,7 @@ class TobaccoUseScreening(ValueSet): } -class FallsScreening(ValueSet): +class FallsScreening(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments using a falls screening tool. @@ -53,7 +64,7 @@ class FallsScreening(ValueSet): } -class StandardizedToolsForAssessmentOfCognition(ValueSet): +class StandardizedToolsForAssessmentOfCognition(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to define concepts for assessments representing total score results for standardized tools used for the evaluation of cognition. @@ -92,7 +103,7 @@ class StandardizedToolsForAssessmentOfCognition(ValueSet): } -class SexuallyActive(ValueSet): +class SexuallyActive(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments to indicate vaginal intercourse. @@ -115,7 +126,7 @@ class SexuallyActive(ValueSet): } -class StandardizedPainAssessmentTool(ValueSet): +class StandardizedPainAssessmentTool(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments using pain-focused tools or instruments to quantify pain intensity. @@ -142,7 +153,7 @@ class StandardizedPainAssessmentTool(ValueSet): } -class Phq9AndPhq9MTools(ValueSet): +class Phq9AndPhq9MTools(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for the assessments of PHQ 9 and PHQ 9M resulting in a completed depression assessment scores for adults and adolescents. @@ -166,7 +177,7 @@ class Phq9AndPhq9MTools(ValueSet): } -class AverageNumberOfDrinksPerDrinkingDay(ValueSet): +class AverageNumberOfDrinksPerDrinkingDay(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments measuring the number of alcoholic drinks per drinking day. @@ -189,7 +200,7 @@ class AverageNumberOfDrinksPerDrinkingDay(ValueSet): } -class HistoryOfHipFractureInParent(ValueSet): +class HistoryOfHipFractureInParent(AssessmentValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for H145 assessments of a family history of hip fracture in a parent. diff --git a/canvas_sdk/value_set/value_set.py b/canvas_sdk/value_set/value_set.py index 9ff8d029..b74bd4db 100644 --- a/canvas_sdk/value_set/value_set.py +++ b/canvas_sdk/value_set/value_set.py @@ -1,6 +1,9 @@ from collections import defaultdict +from collections.abc import Container from typing import Dict, Union, cast +from django.db.models import Q + class CodeConstants: """A class representing different code systems and their URLs.""" @@ -103,4 +106,13 @@ class ValueSet(CodeConstantsURLMapping, metaclass=ValueSystems): """The Base class for a ValueSet.""" values: dict[str, set] - pass + + @staticmethod + def q_object(system: str, codes: Container[str]) -> Q: + """ + Provide the Django Q object for the ValueSet query. + + This method can be overridden if a Q object with different filtering options is needed for a + particular ValueSet. + """ + return Q(codings__system=system, codings__code__in=codes) From d4a85900d90a78914dad8cada9117b9f2135bb24 Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Fri, 1 Nov 2024 10:28:17 -0500 Subject: [PATCH 3/8] Reverted refactoring --- canvas_sdk/v1/data/questionnaire.py | 14 +++++++++++- canvas_sdk/value_set/v2022/assessment.py | 27 +++++++----------------- canvas_sdk/value_set/value_set.py | 13 ------------ 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/canvas_sdk/v1/data/questionnaire.py b/canvas_sdk/v1/data/questionnaire.py index 1bd8f70d..a2215b0a 100644 --- a/canvas_sdk/v1/data/questionnaire.py +++ b/canvas_sdk/v1/data/questionnaire.py @@ -1,4 +1,7 @@ +from collections.abc import Container + from django.db import models +from django.db.models import Q from canvas_sdk.v1.data import Patient from canvas_sdk.v1.data.base import ( @@ -72,6 +75,15 @@ class Meta: code = models.CharField() +class QuestionnaireValueSetLookupQuerySet(ValueSetLookupByNameQuerySet): + """QuerySet class for Questionaire ValueSet lookups.""" + + @staticmethod + def q_object(system: str, codes: Container[str]) -> Q: + """The code system and code values for a Questionnaire are just attributes on the model.""" + return Q(code_system=system, code__in=codes) + + class Questionnaire(models.Model): """Questionnaire.""" @@ -80,7 +92,7 @@ class Meta: app_label = "canvas_sdk" db_table = "canvas_sdk_data_api_questionnaire_001" - objects = models.Manager.from_queryset(ValueSetLookupByNameQuerySet)() + objects = models.Manager.from_queryset(QuestionnaireValueSetLookupQuerySet)() id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/value_set/v2022/assessment.py b/canvas_sdk/value_set/v2022/assessment.py index bf6dce1b..0c0eb54e 100644 --- a/canvas_sdk/value_set/v2022/assessment.py +++ b/canvas_sdk/value_set/v2022/assessment.py @@ -1,18 +1,7 @@ -from typing import Container - -from django.db.models import Q - from ..value_set import ValueSet -class AssessmentValueSet(ValueSet): - @staticmethod - def q_object(system: str, codes: Container[str]) -> Q: - """The code system and code values for a Questionnaire are just attributes on the model.""" - return Q(code_system=system, code__in=codes) - - -class TobaccoUseScreening(AssessmentValueSet): +class TobaccoUseScreening(ValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments to screen for tobacco use. @@ -38,7 +27,7 @@ class TobaccoUseScreening(AssessmentValueSet): } -class FallsScreening(AssessmentValueSet): +class FallsScreening(ValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments using a falls screening tool. @@ -64,7 +53,7 @@ class FallsScreening(AssessmentValueSet): } -class StandardizedToolsForAssessmentOfCognition(AssessmentValueSet): +class StandardizedToolsForAssessmentOfCognition(ValueSet): """ **Clinical Focus:** The purpose of this value set is to define concepts for assessments representing total score results for standardized tools used for the evaluation of cognition. @@ -103,7 +92,7 @@ class StandardizedToolsForAssessmentOfCognition(AssessmentValueSet): } -class SexuallyActive(AssessmentValueSet): +class SexuallyActive(ValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments to indicate vaginal intercourse. @@ -126,7 +115,7 @@ class SexuallyActive(AssessmentValueSet): } -class StandardizedPainAssessmentTool(AssessmentValueSet): +class StandardizedPainAssessmentTool(ValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments using pain-focused tools or instruments to quantify pain intensity. @@ -153,7 +142,7 @@ class StandardizedPainAssessmentTool(AssessmentValueSet): } -class Phq9AndPhq9MTools(AssessmentValueSet): +class Phq9AndPhq9MTools(ValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for the assessments of PHQ 9 and PHQ 9M resulting in a completed depression assessment scores for adults and adolescents. @@ -177,7 +166,7 @@ class Phq9AndPhq9MTools(AssessmentValueSet): } -class AverageNumberOfDrinksPerDrinkingDay(AssessmentValueSet): +class AverageNumberOfDrinksPerDrinkingDay(ValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for assessments measuring the number of alcoholic drinks per drinking day. @@ -200,7 +189,7 @@ class AverageNumberOfDrinksPerDrinkingDay(AssessmentValueSet): } -class HistoryOfHipFractureInParent(AssessmentValueSet): +class HistoryOfHipFractureInParent(ValueSet): """ **Clinical Focus:** The purpose of this value set is to represent concepts for H145 assessments of a family history of hip fracture in a parent. diff --git a/canvas_sdk/value_set/value_set.py b/canvas_sdk/value_set/value_set.py index b74bd4db..323681cf 100644 --- a/canvas_sdk/value_set/value_set.py +++ b/canvas_sdk/value_set/value_set.py @@ -1,9 +1,6 @@ from collections import defaultdict -from collections.abc import Container from typing import Dict, Union, cast -from django.db.models import Q - class CodeConstants: """A class representing different code systems and their URLs.""" @@ -106,13 +103,3 @@ class ValueSet(CodeConstantsURLMapping, metaclass=ValueSystems): """The Base class for a ValueSet.""" values: dict[str, set] - - @staticmethod - def q_object(system: str, codes: Container[str]) -> Q: - """ - Provide the Django Q object for the ValueSet query. - - This method can be overridden if a Q object with different filtering options is needed for a - particular ValueSet. - """ - return Q(codings__system=system, codings__code__in=codes) From e3d9149b67cd0473b7f740f9a0e7088ac01a371f Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Fri, 1 Nov 2024 12:55:37 -0500 Subject: [PATCH 4/8] Added dysrhythmia plugin for review --- hcc004v1_dysrhythmia_suspect.py | 318 ++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 hcc004v1_dysrhythmia_suspect.py diff --git a/hcc004v1_dysrhythmia_suspect.py b/hcc004v1_dysrhythmia_suspect.py new file mode 100644 index 00000000..6d19c13f --- /dev/null +++ b/hcc004v1_dysrhythmia_suspect.py @@ -0,0 +1,318 @@ +from typing import TYPE_CHECKING, Any + +import arrow +from django.db.models import Q + +from canvas_sdk.effects import Effect +from canvas_sdk.effects.protocol_card import ProtocolCard, Recommendation +from canvas_sdk.events import EventType +from canvas_sdk.protocols import BaseProtocol +from canvas_sdk.v1.data.condition import Condition +from canvas_sdk.v1.data.medication import Medication +from canvas_sdk.v1.data.patient import Patient +from canvas_sdk.value_set.value_set import ValueSet + +if TYPE_CHECKING: + from canvas_sdk.effects import Effect + + +# TODO: Find a home for this +class DysrhythmiaClassConditionSuspect(ValueSet): + """Dysrhythmia Class Condition suspect.""" + + VALUE_SET_NAME = "Dysrhythmia Class Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-18" + + ICD10CM = { + "I420", + "I421", + "I422", + "I423", + "I424", + "I425", + "I426", + "I427", + "I428", + "I429", + "I470", + "I471", + "I4710", + "I4711", + "I4719", + "I472", + "I4720", + "I4721", + "I4729", + "I479", + "I480", + "I481", + "I4811", + "I4819", + "I482", + "I4820", + "I4821", + "I483", + "I484", + "I4891", + "I4892", + "I4901", + "I4902", + "I491", + "I492", + "I493", + "I4940", + "I4949", + "I495", + "I498", + "I499", + } + + +# TODO: Find a home for this +class Antiarrhythmics(ValueSet): + """Antiarrhythmics.""" + + VALUE_SET_NAME = "Antiarrhythmics" + EXPANSION_VERSION = "ClassPath Update 18-10-15" + + FDB = { + "150358", + "151807", + "152779", + "155773", + "157601", + "160121", + "165621", + "166591", + "169107", + "169508", + "170461", + "174429", + "175363", + "175494", + "178251", + "183239", + "184929", + "185830", + "189377", + "189730", + "190878", + "193821", + "194412", + "195187", + "196380", + "198310", + "199400", + "203114", + "205183", + "206598", + "208686", + "210260", + "210732", + "212898", + "221901", + "222092", + "223459", + "224332", + "228864", + "230155", + "237183", + "243776", + "243776", + "248491", + "248829", + "250272", + "251530", + "251766", + "260972", + "261266", + "261929", + "262594", + "265464", + "265785", + "274471", + "278255", + "278255", + "278255", + "278255", + "280333", + "281153", + "283306", + "288964", + "291187", + "296991", + "444249", + "444249", + "444944", + "444944", + "449494", + "449496", + "451558", + "451559", + "451560", + "453457", + "453462", + "454178", + "454180", + "454181", + "454205", + "454206", + "454207", + "454371", + "545231", + "545231", + "545232", + "545233", + "545238", + "545239", + "545239", + "558741", + "558745", + "559416", + "560050", + "563304", + "563305", + "563306", + "563310", + "564459", + "564460", + "565068", + "565069", + "573523", + "583982", + "583982", + "583985", + "583985", + "590326", + "590375", + "590376", + "591479", + "592349", + "592421", + "594710", + "594714", + } + + +class Protocol(BaseProtocol): + """Dysrhythmia Suspects.""" + + # TODO: Add ProtocolOverride event types + RESPONDS_TO = [ + EventType.Name(EventType.CONDITION_CREATED), + EventType.Name(EventType.CONDITION_UPDATED), + EventType.Name(EventType.MEDICATION_LIST_ITEM_CREATED), + EventType.Name(EventType.MEDICATION_LIST_ITEM_UPDATED), + ] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.patient_id = self.patient_id_from_event() + + # TODO: Getting the patient or patient ID from an objects seems like something that will happen + # a lot. Should this be standardized in a base class? On the flip side, the fields needed from + # a patient may not be standard. Maybe a simple, standard, efficient way to get a patient ID + # from a target is a good place to start. + def patient_id_from_event(self) -> str: + """Get the patient ID from the event target.""" + # TODO: Add cases for ProtocolOverride + match self.event.type: + case EventType.CONDITION_CREATED | EventType.CONDITION_UPDATED: + model = Condition + case EventType.MEDICATION_LIST_ITEM_CREATED | EventType.MEDICATION_LIST_ITEM_UPDATED: + model = Medication + case _: + raise AssertionError( + f"Event type {self.event.type} not supported by 'patient_id_from_event'" + ) + + return ( + model.objects.select_related("patient") + .values_list("patient__id") + .get(id=self.event.target)[0] + ) + + def in_denominator(self) -> bool: + """ + Patients with any active medication in Antiarrhythmics Drug Class. + """ + # TODO: Settable time frame is not currently supported — where would it come from? + time_frame_provided = False + if not time_frame_provided: + # TODO: Should these datetimes be based on local time or UTC time? + time_frame_end = arrow.get() + time_frame_start = time_frame_end.shift(years=-1) + else: + raise RuntimeError("Time frame is not settable") + + # TODO: Is the status field used at all for medications, or is filtering done just with start/end dates? + qs = Medication.objects.filter(patient__id=self.patient_id, start_date__lte=time_frame_end) + + # TODO: still_active=False is not currently supported — where would the value come from? + still_active = True + if still_active: + return qs.filter(Q(end_date__isnull=True) | Q(end_date__gte=time_frame_end)).exists() + else: + return qs.filter(Q(end_date__isnull=True) | Q(end_date__gte=time_frame_start)).exists() + + def in_numerator(self) -> bool: + """ + Patients without active conditions within the list with ICD 10 I42.* I47.*, I48.*, I49.*. + """ + # TODO: Settable time frame is not currently supported — where would it come from? + time_frame_provided = False + if not time_frame_provided: + # TODO: Should these datetimes be based on local time or UTC time? + time_frame_end = arrow.get() + time_frame_start = time_frame_end.shift(years=-1) + else: + raise RuntimeError("Time frame is not settable") + + # TODO: Is the status field used at all for conditions, or is filtering done just with start/end dates? + qs = Condition.objects.filter(patient__id=self.patient_id, onset_date__lte=time_frame_end) + + # TODO: still_active=False is not currently supported — where would the value come from? + still_active = True + if still_active: + return qs.filter( + Q(resolution_date__isnull=True) | Q(resolution__gte=time_frame_end) + ).exists() + else: + return qs.filter( + Q(resolution_date__isnull=True) | Q(resolution_date__gte=time_frame_start) + ).exists() + + def compute(self) -> list[Effect]: + """Return a ProtocolCard effect if the patient is in both the denominator and numerator.""" + if self.in_denominator(): + if self.in_numerator(): + first_name = Patient.objects.values_list("first_name").get(id=self.patient_id)[0] + + title = "Consider updating the Conditions List to include Dysrhythmia related problem as clinically appropriate." + narrative = f"{first_name} has an active medication on the Medication List commonly used for Dysrhythmia. There is no associated condition on the Conditions List." + + card = ProtocolCard( + patient_id=self.patient_id, + key="HCC004v1_RECOMMEND_DIAGNOSE_DYSRHYTHMIA", + title=title, + narrative=narrative, + recommendations=[ + Recommendation( + title=title, + button="Diagnose", + # TODO: Is the HREF value needed? + # href=None, + command={"key": "diagnose"}, + # TODO: Is the context value needed? + # context=None + ) + ], + status=ProtocolCard.Status.DUE, + ) + + return [card.apply()] + else: + # TODO: Is an effect required if patient is in denominator but not in numerator? + pass + + return [] From c7aa83b92782f6b4f98a9d0629ce648b41e2be87 Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Fri, 1 Nov 2024 16:12:56 -0500 Subject: [PATCH 5/8] Remove gettext_lazy --- canvas_sdk/v1/data/protocol_override.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/canvas_sdk/v1/data/protocol_override.py b/canvas_sdk/v1/data/protocol_override.py index 70a21a84..a128eb78 100644 --- a/canvas_sdk/v1/data/protocol_override.py +++ b/canvas_sdk/v1/data/protocol_override.py @@ -1,5 +1,4 @@ from django.db import models -from django.utils.translation import gettext_lazy as _ from canvas_sdk.v1.data.base import CommittableModelManager from canvas_sdk.v1.data.patient import Patient @@ -9,16 +8,16 @@ class IntervalUnit(models.TextChoices): """ProtocolOverride cycle IntervalUnit.""" - DAYS = "days", _("days") - MONTHS = "months", _("months") - YEARS = "years", _("years") + DAYS = "days", "days" + MONTHS = "months", "months" + YEARS = "years", "years" class Status(models.TextChoices): """ProtocolOverride Status.""" - ACTIVE = "active", _("active") - INACTIVE = "inactive", _("inactive") + ACTIVE = "active", "active" + INACTIVE = "inactive", "inactive" class ProtocolOverride(models.Model): From b139a3b0cb329f905b23cc7a9a134a75ece29f5d Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Wed, 6 Nov 2024 16:39:00 -0600 Subject: [PATCH 6/8] Misc changes for dysrhythmia plugin --- canvas_sdk/v1/data/condition.py | 12 ++ canvas_sdk/v1/data/medication.py | 14 +- canvas_sdk/value_set/__init__.py | 0 canvas_sdk/value_set/custom.py | 177 +++++++++++++++++++++++++ canvas_sdk/value_set/v2022/__init__.py | 0 5 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 canvas_sdk/value_set/__init__.py create mode 100644 canvas_sdk/value_set/custom.py create mode 100644 canvas_sdk/value_set/v2022/__init__.py diff --git a/canvas_sdk/v1/data/condition.py b/canvas_sdk/v1/data/condition.py index 10afd58b..2e6f6d68 100644 --- a/canvas_sdk/v1/data/condition.py +++ b/canvas_sdk/v1/data/condition.py @@ -1,10 +1,21 @@ from django.db import models +from django.db.models import TextChoices from canvas_sdk.v1.data.base import CommittableModelManager, ValueSetLookupQuerySet from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.user import CanvasUser +class ClinicalStatus(TextChoices): + """Condition clinical status.""" + + ACTIVE = "active", "active" + RELAPSE = "relapse", "relapse" + REMISSION = "remission", "remission" + RESOLVED = "resolved", "resolved" + INVESTIGATIVE = "investigative", "investigative" + + class ConditionQuerySet(ValueSetLookupQuerySet): """ConditionQuerySet.""" @@ -25,6 +36,7 @@ class Meta: dbid = models.BigIntegerField(primary_key=True) onset_date = models.DateField() resolution_date = models.DateField() + clinical_status = models.CharField(choices=ClinicalStatus.choices) deleted = models.BooleanField() entered_in_error = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) committer = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) diff --git a/canvas_sdk/v1/data/medication.py b/canvas_sdk/v1/data/medication.py index e13061b2..14ab3a4a 100644 --- a/canvas_sdk/v1/data/medication.py +++ b/canvas_sdk/v1/data/medication.py @@ -1,10 +1,18 @@ from django.db import models +from django.db.models import TextChoices from canvas_sdk.v1.data.base import CommittableModelManager, ValueSetLookupQuerySet from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.user import CanvasUser +class Status(TextChoices): + """Medication status.""" + + ACTIVE = "active", "active" + INACTIVE = "inactive", "inactive" + + class MedicationQuerySet(ValueSetLookupQuerySet): """MedicationQuerySet.""" @@ -27,9 +35,9 @@ class Meta: deleted = models.BooleanField() entered_in_error = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) committer = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) - status = models.CharField() - start_date = models.DateField() - end_date = models.DateField() + status = models.CharField(choices=Status.choices) + start_date = models.DateTimeField() + end_date = models.DateTimeField() quantity_qualifier_description = models.CharField() clinical_quantity_description = models.CharField() potency_unit_code = models.CharField() diff --git a/canvas_sdk/value_set/__init__.py b/canvas_sdk/value_set/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/canvas_sdk/value_set/custom.py b/canvas_sdk/value_set/custom.py new file mode 100644 index 00000000..759cbc58 --- /dev/null +++ b/canvas_sdk/value_set/custom.py @@ -0,0 +1,177 @@ +from canvas_sdk.value_set.value_set import ValueSet + + +class DysrhythmiaClassConditionSuspect(ValueSet): + """Dysrhythmia Class Condition suspect.""" + + VALUE_SET_NAME = "Dysrhythmia Class Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2024-11-04" + + ICD10CM = { + "I420", + "I421", + "I422", + "I423", + "I424", + "I425", + "I426", + "I427", + "I428", + "I429", + "I470", + "I471", + "I4710", + "I4711", + "I4719", + "I472", + "I4720", + "I4721", + "I4729", + "I479", + "I480", + "I481", + "I4811", + "I4819", + "I482", + "I4820", + "I4821", + "I483", + "I484", + "I4891", + "I4892", + "I4901", + "I4902", + "I491", + "I492", + "I493", + "I4940", + "I4949", + "I495", + "I498", + "I499", + } + + +class Antiarrhythmics(ValueSet): + """Antiarrhythmics.""" + + VALUE_SET_NAME = "Antiarrhythmics" + EXPANSION_VERSION = "ClassPath Update 18-10-15" + + FDB = { + "150358", + "151807", + "152779", + "155773", + "157601", + "160121", + "165621", + "166591", + "169107", + "169508", + "170461", + "174429", + "175363", + "175494", + "178251", + "183239", + "184929", + "185830", + "189377", + "189730", + "190878", + "193821", + "194412", + "195187", + "196380", + "198310", + "199400", + "203114", + "205183", + "206598", + "208686", + "210260", + "210732", + "212898", + "221901", + "222092", + "223459", + "224332", + "228864", + "230155", + "237183", + "243776", + "243776", + "248491", + "248829", + "250272", + "251530", + "251766", + "260972", + "261266", + "261929", + "262594", + "265464", + "265785", + "274471", + "278255", + "278255", + "278255", + "278255", + "280333", + "281153", + "283306", + "288964", + "291187", + "296991", + "444249", + "444249", + "444944", + "444944", + "449494", + "449496", + "451558", + "451559", + "451560", + "453457", + "453462", + "454178", + "454180", + "454181", + "454205", + "454206", + "454207", + "454371", + "545231", + "545231", + "545232", + "545233", + "545238", + "545239", + "545239", + "558741", + "558745", + "559416", + "560050", + "563304", + "563305", + "563306", + "563310", + "564459", + "564460", + "565068", + "565069", + "573523", + "583982", + "583982", + "583985", + "583985", + "590326", + "590375", + "590376", + "591479", + "592349", + "592421", + "594710", + "594714", + } diff --git a/canvas_sdk/value_set/v2022/__init__.py b/canvas_sdk/value_set/v2022/__init__.py new file mode 100644 index 00000000..e69de29b From fe192d4d924132ab5d403f0f6110fd01a37b7c3a Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Thu, 7 Nov 2024 12:39:52 -0600 Subject: [PATCH 7/8] Reverted a few changes --- canvas_sdk/v1/data/condition.py | 12 --- canvas_sdk/v1/data/medication.py | 14 +-- canvas_sdk/value_set/custom.py | 177 ------------------------------- 3 files changed, 3 insertions(+), 200 deletions(-) delete mode 100644 canvas_sdk/value_set/custom.py diff --git a/canvas_sdk/v1/data/condition.py b/canvas_sdk/v1/data/condition.py index 2e6f6d68..10afd58b 100644 --- a/canvas_sdk/v1/data/condition.py +++ b/canvas_sdk/v1/data/condition.py @@ -1,21 +1,10 @@ from django.db import models -from django.db.models import TextChoices from canvas_sdk.v1.data.base import CommittableModelManager, ValueSetLookupQuerySet from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.user import CanvasUser -class ClinicalStatus(TextChoices): - """Condition clinical status.""" - - ACTIVE = "active", "active" - RELAPSE = "relapse", "relapse" - REMISSION = "remission", "remission" - RESOLVED = "resolved", "resolved" - INVESTIGATIVE = "investigative", "investigative" - - class ConditionQuerySet(ValueSetLookupQuerySet): """ConditionQuerySet.""" @@ -36,7 +25,6 @@ class Meta: dbid = models.BigIntegerField(primary_key=True) onset_date = models.DateField() resolution_date = models.DateField() - clinical_status = models.CharField(choices=ClinicalStatus.choices) deleted = models.BooleanField() entered_in_error = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) committer = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) diff --git a/canvas_sdk/v1/data/medication.py b/canvas_sdk/v1/data/medication.py index 14ab3a4a..e13061b2 100644 --- a/canvas_sdk/v1/data/medication.py +++ b/canvas_sdk/v1/data/medication.py @@ -1,18 +1,10 @@ from django.db import models -from django.db.models import TextChoices from canvas_sdk.v1.data.base import CommittableModelManager, ValueSetLookupQuerySet from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.user import CanvasUser -class Status(TextChoices): - """Medication status.""" - - ACTIVE = "active", "active" - INACTIVE = "inactive", "inactive" - - class MedicationQuerySet(ValueSetLookupQuerySet): """MedicationQuerySet.""" @@ -35,9 +27,9 @@ class Meta: deleted = models.BooleanField() entered_in_error = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) committer = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) - status = models.CharField(choices=Status.choices) - start_date = models.DateTimeField() - end_date = models.DateTimeField() + status = models.CharField() + start_date = models.DateField() + end_date = models.DateField() quantity_qualifier_description = models.CharField() clinical_quantity_description = models.CharField() potency_unit_code = models.CharField() diff --git a/canvas_sdk/value_set/custom.py b/canvas_sdk/value_set/custom.py deleted file mode 100644 index 759cbc58..00000000 --- a/canvas_sdk/value_set/custom.py +++ /dev/null @@ -1,177 +0,0 @@ -from canvas_sdk.value_set.value_set import ValueSet - - -class DysrhythmiaClassConditionSuspect(ValueSet): - """Dysrhythmia Class Condition suspect.""" - - VALUE_SET_NAME = "Dysrhythmia Class Condition suspect" - EXPANSION_VERSION = "CanvasHCC Update 2024-11-04" - - ICD10CM = { - "I420", - "I421", - "I422", - "I423", - "I424", - "I425", - "I426", - "I427", - "I428", - "I429", - "I470", - "I471", - "I4710", - "I4711", - "I4719", - "I472", - "I4720", - "I4721", - "I4729", - "I479", - "I480", - "I481", - "I4811", - "I4819", - "I482", - "I4820", - "I4821", - "I483", - "I484", - "I4891", - "I4892", - "I4901", - "I4902", - "I491", - "I492", - "I493", - "I4940", - "I4949", - "I495", - "I498", - "I499", - } - - -class Antiarrhythmics(ValueSet): - """Antiarrhythmics.""" - - VALUE_SET_NAME = "Antiarrhythmics" - EXPANSION_VERSION = "ClassPath Update 18-10-15" - - FDB = { - "150358", - "151807", - "152779", - "155773", - "157601", - "160121", - "165621", - "166591", - "169107", - "169508", - "170461", - "174429", - "175363", - "175494", - "178251", - "183239", - "184929", - "185830", - "189377", - "189730", - "190878", - "193821", - "194412", - "195187", - "196380", - "198310", - "199400", - "203114", - "205183", - "206598", - "208686", - "210260", - "210732", - "212898", - "221901", - "222092", - "223459", - "224332", - "228864", - "230155", - "237183", - "243776", - "243776", - "248491", - "248829", - "250272", - "251530", - "251766", - "260972", - "261266", - "261929", - "262594", - "265464", - "265785", - "274471", - "278255", - "278255", - "278255", - "278255", - "280333", - "281153", - "283306", - "288964", - "291187", - "296991", - "444249", - "444249", - "444944", - "444944", - "449494", - "449496", - "451558", - "451559", - "451560", - "453457", - "453462", - "454178", - "454180", - "454181", - "454205", - "454206", - "454207", - "454371", - "545231", - "545231", - "545232", - "545233", - "545238", - "545239", - "545239", - "558741", - "558745", - "559416", - "560050", - "563304", - "563305", - "563306", - "563310", - "564459", - "564460", - "565068", - "565069", - "573523", - "583982", - "583982", - "583985", - "583985", - "590326", - "590375", - "590376", - "591479", - "592349", - "592421", - "594710", - "594714", - } From a3aafc42bf2c5b11ba8a7d77c8f179fa0aa652e7 Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Mon, 11 Nov 2024 15:01:01 -0600 Subject: [PATCH 8/8] Deleted file --- hcc004v1_dysrhythmia_suspect.py | 318 -------------------------------- 1 file changed, 318 deletions(-) delete mode 100644 hcc004v1_dysrhythmia_suspect.py diff --git a/hcc004v1_dysrhythmia_suspect.py b/hcc004v1_dysrhythmia_suspect.py deleted file mode 100644 index 6d19c13f..00000000 --- a/hcc004v1_dysrhythmia_suspect.py +++ /dev/null @@ -1,318 +0,0 @@ -from typing import TYPE_CHECKING, Any - -import arrow -from django.db.models import Q - -from canvas_sdk.effects import Effect -from canvas_sdk.effects.protocol_card import ProtocolCard, Recommendation -from canvas_sdk.events import EventType -from canvas_sdk.protocols import BaseProtocol -from canvas_sdk.v1.data.condition import Condition -from canvas_sdk.v1.data.medication import Medication -from canvas_sdk.v1.data.patient import Patient -from canvas_sdk.value_set.value_set import ValueSet - -if TYPE_CHECKING: - from canvas_sdk.effects import Effect - - -# TODO: Find a home for this -class DysrhythmiaClassConditionSuspect(ValueSet): - """Dysrhythmia Class Condition suspect.""" - - VALUE_SET_NAME = "Dysrhythmia Class Condition suspect" - EXPANSION_VERSION = "CanvasHCC Update 2018-10-18" - - ICD10CM = { - "I420", - "I421", - "I422", - "I423", - "I424", - "I425", - "I426", - "I427", - "I428", - "I429", - "I470", - "I471", - "I4710", - "I4711", - "I4719", - "I472", - "I4720", - "I4721", - "I4729", - "I479", - "I480", - "I481", - "I4811", - "I4819", - "I482", - "I4820", - "I4821", - "I483", - "I484", - "I4891", - "I4892", - "I4901", - "I4902", - "I491", - "I492", - "I493", - "I4940", - "I4949", - "I495", - "I498", - "I499", - } - - -# TODO: Find a home for this -class Antiarrhythmics(ValueSet): - """Antiarrhythmics.""" - - VALUE_SET_NAME = "Antiarrhythmics" - EXPANSION_VERSION = "ClassPath Update 18-10-15" - - FDB = { - "150358", - "151807", - "152779", - "155773", - "157601", - "160121", - "165621", - "166591", - "169107", - "169508", - "170461", - "174429", - "175363", - "175494", - "178251", - "183239", - "184929", - "185830", - "189377", - "189730", - "190878", - "193821", - "194412", - "195187", - "196380", - "198310", - "199400", - "203114", - "205183", - "206598", - "208686", - "210260", - "210732", - "212898", - "221901", - "222092", - "223459", - "224332", - "228864", - "230155", - "237183", - "243776", - "243776", - "248491", - "248829", - "250272", - "251530", - "251766", - "260972", - "261266", - "261929", - "262594", - "265464", - "265785", - "274471", - "278255", - "278255", - "278255", - "278255", - "280333", - "281153", - "283306", - "288964", - "291187", - "296991", - "444249", - "444249", - "444944", - "444944", - "449494", - "449496", - "451558", - "451559", - "451560", - "453457", - "453462", - "454178", - "454180", - "454181", - "454205", - "454206", - "454207", - "454371", - "545231", - "545231", - "545232", - "545233", - "545238", - "545239", - "545239", - "558741", - "558745", - "559416", - "560050", - "563304", - "563305", - "563306", - "563310", - "564459", - "564460", - "565068", - "565069", - "573523", - "583982", - "583982", - "583985", - "583985", - "590326", - "590375", - "590376", - "591479", - "592349", - "592421", - "594710", - "594714", - } - - -class Protocol(BaseProtocol): - """Dysrhythmia Suspects.""" - - # TODO: Add ProtocolOverride event types - RESPONDS_TO = [ - EventType.Name(EventType.CONDITION_CREATED), - EventType.Name(EventType.CONDITION_UPDATED), - EventType.Name(EventType.MEDICATION_LIST_ITEM_CREATED), - EventType.Name(EventType.MEDICATION_LIST_ITEM_UPDATED), - ] - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.patient_id = self.patient_id_from_event() - - # TODO: Getting the patient or patient ID from an objects seems like something that will happen - # a lot. Should this be standardized in a base class? On the flip side, the fields needed from - # a patient may not be standard. Maybe a simple, standard, efficient way to get a patient ID - # from a target is a good place to start. - def patient_id_from_event(self) -> str: - """Get the patient ID from the event target.""" - # TODO: Add cases for ProtocolOverride - match self.event.type: - case EventType.CONDITION_CREATED | EventType.CONDITION_UPDATED: - model = Condition - case EventType.MEDICATION_LIST_ITEM_CREATED | EventType.MEDICATION_LIST_ITEM_UPDATED: - model = Medication - case _: - raise AssertionError( - f"Event type {self.event.type} not supported by 'patient_id_from_event'" - ) - - return ( - model.objects.select_related("patient") - .values_list("patient__id") - .get(id=self.event.target)[0] - ) - - def in_denominator(self) -> bool: - """ - Patients with any active medication in Antiarrhythmics Drug Class. - """ - # TODO: Settable time frame is not currently supported — where would it come from? - time_frame_provided = False - if not time_frame_provided: - # TODO: Should these datetimes be based on local time or UTC time? - time_frame_end = arrow.get() - time_frame_start = time_frame_end.shift(years=-1) - else: - raise RuntimeError("Time frame is not settable") - - # TODO: Is the status field used at all for medications, or is filtering done just with start/end dates? - qs = Medication.objects.filter(patient__id=self.patient_id, start_date__lte=time_frame_end) - - # TODO: still_active=False is not currently supported — where would the value come from? - still_active = True - if still_active: - return qs.filter(Q(end_date__isnull=True) | Q(end_date__gte=time_frame_end)).exists() - else: - return qs.filter(Q(end_date__isnull=True) | Q(end_date__gte=time_frame_start)).exists() - - def in_numerator(self) -> bool: - """ - Patients without active conditions within the list with ICD 10 I42.* I47.*, I48.*, I49.*. - """ - # TODO: Settable time frame is not currently supported — where would it come from? - time_frame_provided = False - if not time_frame_provided: - # TODO: Should these datetimes be based on local time or UTC time? - time_frame_end = arrow.get() - time_frame_start = time_frame_end.shift(years=-1) - else: - raise RuntimeError("Time frame is not settable") - - # TODO: Is the status field used at all for conditions, or is filtering done just with start/end dates? - qs = Condition.objects.filter(patient__id=self.patient_id, onset_date__lte=time_frame_end) - - # TODO: still_active=False is not currently supported — where would the value come from? - still_active = True - if still_active: - return qs.filter( - Q(resolution_date__isnull=True) | Q(resolution__gte=time_frame_end) - ).exists() - else: - return qs.filter( - Q(resolution_date__isnull=True) | Q(resolution_date__gte=time_frame_start) - ).exists() - - def compute(self) -> list[Effect]: - """Return a ProtocolCard effect if the patient is in both the denominator and numerator.""" - if self.in_denominator(): - if self.in_numerator(): - first_name = Patient.objects.values_list("first_name").get(id=self.patient_id)[0] - - title = "Consider updating the Conditions List to include Dysrhythmia related problem as clinically appropriate." - narrative = f"{first_name} has an active medication on the Medication List commonly used for Dysrhythmia. There is no associated condition on the Conditions List." - - card = ProtocolCard( - patient_id=self.patient_id, - key="HCC004v1_RECOMMEND_DIAGNOSE_DYSRHYTHMIA", - title=title, - narrative=narrative, - recommendations=[ - Recommendation( - title=title, - button="Diagnose", - # TODO: Is the HREF value needed? - # href=None, - command={"key": "diagnose"}, - # TODO: Is the context value needed? - # context=None - ) - ], - status=ProtocolCard.Status.DUE, - ) - - return [card.apply()] - else: - # TODO: Is an effect required if patient is in denominator but not in numerator? - pass - - return []