From 500bc81948bcfe3e6ef7e6cd0a1e8ae527adbf76 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Wed, 6 Nov 2024 14:49:38 -0800 Subject: [PATCH 01/15] feat: adds functionality for the hcc annual wellness visit protocol to be a plugin (#177) --- .../effects/protocol_card/protocol_card.py | 2 + canvas_sdk/effects/protocol_card/tests.py | 4 +- canvas_sdk/protocols/base.py | 2 + .../protocols/clinical_quality_measure.py | 17 ++ canvas_sdk/protocols/timeframe.py | 39 +++++ canvas_sdk/v1/data/__init__.py | 1 + canvas_sdk/v1/data/base.py | 21 +++ canvas_sdk/v1/data/billing.py | 56 +++++++ canvas_sdk/v1/data/note.py | 152 ++++++++++++++++++ canvas_sdk/v1/data/patient.py | 18 +++ canvas_sdk/value_set/custom.py | 15 ++ canvas_sdk/value_set/value_set.py | 3 + 12 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 canvas_sdk/protocols/timeframe.py create mode 100644 canvas_sdk/v1/data/billing.py create mode 100644 canvas_sdk/v1/data/note.py create mode 100644 canvas_sdk/value_set/custom.py diff --git a/canvas_sdk/effects/protocol_card/protocol_card.py b/canvas_sdk/effects/protocol_card/protocol_card.py index 7d519b46..92438bbb 100644 --- a/canvas_sdk/effects/protocol_card/protocol_card.py +++ b/canvas_sdk/effects/protocol_card/protocol_card.py @@ -51,6 +51,7 @@ class Meta: recommendations: list[Recommendation] = [] status: Status = Status.DUE feedback_enabled: bool = False + due_in: int = -1 @property def values(self) -> dict[str, Any]: @@ -63,6 +64,7 @@ def values(self) -> dict[str, Any]: ], "status": self.status.value, "feedback_enabled": self.feedback_enabled, + "due_in": self.due_in, } @property diff --git a/canvas_sdk/effects/protocol_card/tests.py b/canvas_sdk/effects/protocol_card/tests.py index 16ca27c4..3083e632 100644 --- a/canvas_sdk/effects/protocol_card/tests.py +++ b/canvas_sdk/effects/protocol_card/tests.py @@ -25,7 +25,7 @@ def test_apply_method_succeeds_with_patient_id_and_key() -> None: applied = p.apply() assert ( applied.payload - == '{"patient": "uuid", "patient_filter": null, "key": "something-unique", "data": {"title": "", "narrative": "", "recommendations": [], "status": "due", "feedback_enabled": false}}' + == '{"patient": "uuid", "patient_filter": null, "key": "something-unique", "data": {"title": "", "narrative": "", "recommendations": [], "status": "due", "feedback_enabled": false, "due_in": -1}}' ) @@ -112,7 +112,6 @@ def test_add_recommendations( p = ProtocolCard(**init_params) p.add_recommendation(**rec1_params) p.recommendations.append(Recommendation(**rec2_params)) - assert p.values == { "title": init_params["title"], "narrative": init_params["narrative"], @@ -135,6 +134,7 @@ def test_add_recommendations( }, ], "status": "due", + "due_in": -1, "feedback_enabled": False, } diff --git a/canvas_sdk/protocols/base.py b/canvas_sdk/protocols/base.py index 1a85a5db..8873c316 100644 --- a/canvas_sdk/protocols/base.py +++ b/canvas_sdk/protocols/base.py @@ -1,5 +1,7 @@ from typing import Any +import arrow + from canvas_sdk.data.client import GQL_CLIENT from canvas_sdk.handlers.base import BaseHandler diff --git a/canvas_sdk/protocols/clinical_quality_measure.py b/canvas_sdk/protocols/clinical_quality_measure.py index 2ef13185..52781072 100644 --- a/canvas_sdk/protocols/clinical_quality_measure.py +++ b/canvas_sdk/protocols/clinical_quality_measure.py @@ -1,6 +1,9 @@ from typing import Any +import arrow + from canvas_sdk.protocols.base import BaseProtocol +from canvas_sdk.protocols.timeframe import Timeframe class ClinicalQualityMeasure(BaseProtocol): @@ -41,3 +44,17 @@ def protocol_key(cls) -> str: External key used to identify the protocol. """ return cls.__name__ + + @property + def timeframe(self) -> Timeframe: + """The default Timeframe (self.timeframe) for all protocols. + This defaults to have a start of 1 year ago and an end time of the current time. + Plugin authors can override this if a different timeframe is desired. + """ + end = self.now + return Timeframe(start=end.shift(years=-1), end=end) + + @property + def now(self) -> arrow.Arrow: + """A convenience method for returning the current datetime.""" + return arrow.utcnow() diff --git a/canvas_sdk/protocols/timeframe.py b/canvas_sdk/protocols/timeframe.py new file mode 100644 index 00000000..c88a1769 --- /dev/null +++ b/canvas_sdk/protocols/timeframe.py @@ -0,0 +1,39 @@ +import arrow + + +class Timeframe: + """A class representing a timeframe with a start and and end.""" + + def __init__(self, start: arrow.Arrow, end: arrow.Arrow): + self.start = start + self.end = end + + def __str__(self) -> str: + return f"" + + @property + def duration(self) -> int: + """Returns the number of days in the timeframe.""" + return (self.end - self.start).days + + def increased_by(self, years: int = 0, months: int = 0, days: int = 0) -> "Timeframe": + """Returns a new Timeframe object increased by the years, months, days in the arguments.""" + start = self.start + end = self.end + + if years > 0: + end = end.shift(years=years) + elif years < 0: + start = start.shift(years=years) + + if months > 0: + end = end.shift(months=months) + elif months < 0: + start = start.shift(months=months) + + if days > 0: + end = end.shift(days=days) + elif days < 0: + start = start.shift(days=days) + + return Timeframe(start=start, end=end) diff --git a/canvas_sdk/v1/data/__init__.py b/canvas_sdk/v1/data/__init__.py index aa5cd98c..9147f052 100644 --- a/canvas_sdk/v1/data/__init__.py +++ b/canvas_sdk/v1/data/__init__.py @@ -1,3 +1,4 @@ +from .billing import BillingLineItem from .condition import Condition, ConditionCoding from .medication import Medication, MedicationCoding from .patient import Patient diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index 5259f2d3..d66fe4e5 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -5,6 +5,7 @@ from django.db.models import Q if TYPE_CHECKING: + from canvas_sdk.protocols.timeframe import Timeframe from canvas_sdk.value_set.value_set import ValueSet @@ -94,3 +95,23 @@ def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: if i[0] in values_dict ), ) + + +class TimeframeLookupQuerySet(models.QuerySet): + """A class that adds queryset functionality to filter using timeframes.""" + + @property + def timeframe_filter_field(self) -> str: + """Returns the field that should be filtered on. Can be overridden for different models.""" + return "note__datetime_of_service" + + def within(self, timeframe: "Timeframe") -> models.QuerySet: + """A method to filter a queryset for datetimes within a timeframe.""" + return self.filter( + **{ + f"{self.timeframe_filter_field}__range": ( + timeframe.start.datetime, + timeframe.end.datetime, + ) + } + ) diff --git a/canvas_sdk/v1/data/billing.py b/canvas_sdk/v1/data/billing.py new file mode 100644 index 00000000..526dc5d0 --- /dev/null +++ b/canvas_sdk/v1/data/billing.py @@ -0,0 +1,56 @@ +from typing import TYPE_CHECKING, Type + +from django.db import models + +from canvas_sdk.v1.data.base import TimeframeLookupQuerySet, ValueSetLookupQuerySet +from canvas_sdk.v1.data.note import Note +from canvas_sdk.v1.data.patient import Patient +from canvas_sdk.value_set.value_set import CodeConstants + +if TYPE_CHECKING: + from canvas_sdk.value_set.value_set import ValueSet + + +class BillingLineItemQuerySet(ValueSetLookupQuerySet, TimeframeLookupQuerySet): + """A class that adds functionality to filter BillingLineItem objects.""" + + def find(self, value_set: Type["ValueSet"]) -> models.QuerySet: + """ + This method is overridden to use for BillingLineItem CPT codes. + The codes are saved as string values in the BillingLineItem.cpt field, + which differs from other coding models. + """ + values_dict = value_set.values + return self.filter(cpt__in=values_dict.get(CodeConstants.HCPCS, [])) + + +class BillingLineItem(models.Model): + """BillingLineItem.""" + + class BillingLineItemStatus(models.TextChoices): + ACTIVE = "active", "Active" + REMOVED = "removed", "Removed" + + class Meta: + managed = False + app_label = "canvas_sdk" + db_table = "canvas_sdk_data_api_billinglineitem_001" + + # objects = BillingLineItemQuerySet.as_manager() + objects = models.Manager().from_queryset(BillingLineItemQuerySet)() + + id = models.UUIDField() + dbid = models.BigIntegerField(primary_key=True) + created = models.DateTimeField() + modified = models.DateTimeField() + note = models.ForeignKey(Note, on_delete=models.DO_NOTHING, related_name="billing_line_items") + patient = models.ForeignKey( + Patient, on_delete=models.DO_NOTHING, related_name="billing_line_items" + ) + cpt = models.CharField() + charge = models.DecimalField() + description = models.CharField() + units = models.IntegerField() + command_type = models.CharField() + command_id = models.IntegerField() + status = models.CharField(choices=BillingLineItemStatus) diff --git a/canvas_sdk/v1/data/note.py b/canvas_sdk/v1/data/note.py new file mode 100644 index 00000000..bbc91305 --- /dev/null +++ b/canvas_sdk/v1/data/note.py @@ -0,0 +1,152 @@ +from django.contrib.postgres.fields import ArrayField +from django.db import models + +from canvas_sdk.v1.data.patient import Patient + +# from canvas_sdk.v1.data.staff import Staff +from canvas_sdk.v1.data.user import CanvasUser + + +class NoteType(models.Model): + """NoteType.""" + + class NoteTypeCategories(models.TextChoices): + MESSAGE = "message", "Message" + LETTER = "letter", "Letter" + INPATIENT = "inpatient", "Inpatient Visit Note" + REVIEW = "review", "Chart Review Note" + ENCOUNTER = "encounter", "Encounter Note" + APPOINTMENT = "appointment", "Appointment Note" + TASK = "task", "Task" + DATA = "data", "Data" + CCDA = "ccda", "C-CDA" + SCHEDULE_EVENT = "schedule_event", "Schedule Event" + + class PracticeLocationPOS(models.TextChoices): + PHARMACY = "01", "Pharmacy" + TELEHEALTH = "02", "Telehealth" + SCHOOL = "03", "Education Facility" + HOMELESS_SHELTER = "04", "Homeless Shelter" + PRISON = "09", "Prison" + TELEHEALTH_IN_PATIENT_HOME = "10", "Telehealth in Patient's Home" + OFFICE = "11", "Office" + HOME = "12", "Home" + ASSISTED_LIVING = "13", "Asssisted Living Facility" + GROUP_HOME = "14", "Group Home" + MOBILE = "15", "Mobile Unit" + WALK_IN_RETAIL = "17", "Walk-In Retail Health Clinic" + OFF_CAMPUS_OUTPATIENT_HOSPITAL = "19", "Off-Campus Outpatient Hospital" + URGENT_CARE = "20", "Urgent Care Facility" + INPATIENT_HOSPITAL = "21", "Inpatient Hospital" + ON_CAMPUS_OUTPATIENT_HOSPITAL = "22", "On-Campus Outpatient Hospital" + ER_HOSPITAL = "23", "Emergency Room Hospital" + AMBULATORY_SURGERY_CENTER = "24", "Ambulatory Surgery Center" + BIRTHING_CENTER = "25", "Birthing Center" + MILITARY_FACILITY = "26", "Military Treatment Facility" + STREET = "27", "Outreach Site / Street" + SNF = "31", "Skilled Nursing Facility" + NURSING = "32", "Nursing Facility" + CUSTODIAL = "33", "Custodial Care Facility" + HOSPICE = "34", "Hospice" + AMBULANCE_LAND = "41", "Ambulance Land" + AMBULANCE_AIR_WATER = "42", "Ambulance Air or Water" + INDEPENDENT_CLINIC = "49", "Independent Clinic" + FQHC = "50", "Federally Qualified Health Center" + PSYCH = "51", "Inpatient Psychiatric Facility" + PSYCH_PARTIAL = "52", "Inpatient Psychiatric Facility - Partial Hospitalization" + MENTAL_HEALTH_CENTER = "53", "Community Mental Health Center" + INTERMEDIATE_MENTAL = "54", "Intermediate Care Facility for Mentally Retarded" + SUBSTANCE_RESIDENTIAL = "55", "Residential Substance Abuse Treatment Facility" + PSYCH_RESIDENTIAL = "56", "Psychiatric Residential Treatment Center" + SUBSTANCE_NON_RESIDENTIAL = "57", "Non-Residential Substance Abuse Treatment Facility" + MASS_IMMUNIZATION = "60", "Mass Immunization Center" + INPATIENT_REHAB = "61", "Inpatient Rehabilitation Facility" + OUTPATIENT_REHAB = "62", "Outpatient Rehabilitation Facility" + ESRD = "65", "End-Stage Renal Disease Treatment Facility" + PUBLIC_CLINIC = "71", "State or Local Public Health Clinic" + RURAL_CLINIC = "72", "Rural Health Clinic" + INDEPENDENT_LAB = "81", "Independent Laboratory" + OTHER = "99", "Other Place of Service" + + class Meta: + managed = False + app_label = "canvas_sdk" + db_table = "canvas_sdk_data_api_notetype_001" + + dbid = models.BigIntegerField(primary_key=True) + created = models.DateTimeField() + modified = models.DateTimeField() + system = models.CharField() + version = models.CharField() + code = models.CharField() + display = models.CharField() + user_selected = models.BooleanField() + name = models.CharField() + icon = models.CharField() + category = models.CharField(choices=NoteTypeCategories) + rank = models.PositiveIntegerField() + is_default_appointment_type = models.BooleanField() + is_scheduleable = models.BooleanField() + is_telehealth = models.BooleanField() + is_billable = models.BooleanField() + defer_place_of_service_to_practice_location = models.BooleanField() + available_places_of_service = ArrayField(models.CharField(choices=PracticeLocationPOS)) + default_place_of_service = models.CharField(choices=PracticeLocationPOS) + is_system_managed = models.BooleanField() + is_visible = models.BooleanField() + is_active = models.BooleanField() + unique_identifier = models.UUIDField() + deprecated_at = models.DateTimeField() + is_patient_required = models.BooleanField() + allow_custom_title = models.BooleanField() + + +class Note(models.Model): + """Note.""" + + class NoteType(models.TextChoices): + MESSAGE = "message", "Message" + LETTER = "letter", "Letter" + INPATIENT = "inpatient", "Inpatient Visit Note" + REVIEW = "review", "Chart Review Note" + VOICE = "voice", "Phone Call Note" + VIDEO = "video", "Video Call Note" + OFFICE = "office", "Office Visit Note" + LAB = "lab", "Lab Visit Note" + HOME = "home", "Home Visit Note" + GROUP = "group", "Group Visit Note" + APPOINTMENT = "appointment", "Appointment Note" + OFFSITE = "offsite", "Other Offsite Visit Note" + SEARCH = "search", "Search" + TASK = "task", "Task" + DATA = "data", "Data" + CCDA = "ccda", "C-CDA Import" + + class Meta: + managed = False + app_label = "canvas_sdk" + db_table = "canvas_sdk_data_api_note_001" + + id = models.CharField(max_length=32) + dbid = models.BigIntegerField(db_column="dbid", primary_key=True) + created = models.DateTimeField() + modified = models.DateTimeField() + patient = models.ForeignKey(Patient, on_delete=models.DO_NOTHING) + # provider = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="notes") + note_type = models.CharField(choices=NoteType, null=True) + note_type_version = models.ForeignKey( + "NoteType", on_delete=models.DO_NOTHING, related_name="notes" + ) + title = models.TextField() + body = models.JSONField() + originator = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING) + # last_modified_by_staff = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, null=True) + checksum = models.CharField() + billing_note = models.TextField() + # TODO -implement InpatientStay model + # inpatient_stay = models.ForeignKey("InpatientStay", on_delete=models.DO_NOTHING) + related_data = models.JSONField() + # TODO -implement PracticeLocation model + # location = models.ForeignKey(PracticeLocation, on_delete=models.DO_NOTHING) + datetime_of_service = models.DateTimeField() + place_of_service = models.CharField() diff --git a/canvas_sdk/v1/data/patient.py b/canvas_sdk/v1/data/patient.py index 46292c0e..09d7414f 100644 --- a/canvas_sdk/v1/data/patient.py +++ b/canvas_sdk/v1/data/patient.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, Self +import arrow from django.db import models @@ -53,3 +54,20 @@ def find(cls, id: str) -> Self: def __str__(self) -> str: return f"{self.first_name} {self.last_name}" + + def age_at(self, time: arrow.Arrow) -> float: + """Given a datetime, returns what the patient's age would be at that datetime.""" + age = 0 + birth_date = arrow.get(self.birth_date) + if birth_date.date() < time.date(): + age = time.datetime.year - birth_date.datetime.year + if time.datetime.month < birth_date.datetime.month or ( + time.datetime.month == birth_date.datetime.month + and time.datetime.day < birth_date.datetime.day + ): + age -= 1 + + current_year = birth_date.shift(years=age) + next_year = birth_date.shift(years=age + 1) + age += (time.date() - current_year.date()) / (next_year.date() - current_year.date()) + return age diff --git a/canvas_sdk/value_set/custom.py b/canvas_sdk/value_set/custom.py new file mode 100644 index 00000000..6e87a624 --- /dev/null +++ b/canvas_sdk/value_set/custom.py @@ -0,0 +1,15 @@ +from canvas_sdk.value_set.value_set import ValueSet + + +class Hcc005v1AnnualWellnessVisit(ValueSet): + """Hcc005v1AnnualWellnessVisit.""" + + VALUE_SET_NAME = "Annual Wellness Visit" + + HCPCS = { + "G0438", + "G0439", + "G0402", + "99387", + "99397", + } diff --git a/canvas_sdk/value_set/value_set.py b/canvas_sdk/value_set/value_set.py index 60f5d9cb..12b4af40 100644 --- a/canvas_sdk/value_set/value_set.py +++ b/canvas_sdk/value_set/value_set.py @@ -8,6 +8,7 @@ class CodeConstants: """A class representing different code systems and their URLs.""" CPT = "CPT" + HCPCS = "HCPCS" HCPCSLEVELII = "HCPCSLEVELII" CVX = "CVX" LOINC = "LOINC" @@ -22,6 +23,7 @@ class CodeConstants: NDC = "NDC" URL_CPT = "http://www.ama-assn.org/go/cpt" + URL_HCPCS = "http://www.cms.gov/medicare/coding/medhcpcsgeninfo" URL_HCPCSLEVELII = "https://coder.aapc.com/hcpcs-codes" URL_CVX = "http://hl7.org/fhir/sid/cvx" URL_LOINC = "http://loinc.org" @@ -40,6 +42,7 @@ class CodeConstantsURLMappingMixin: CODE_SYSTEM_MAPPING = { CodeConstants.CPT: CodeConstants.URL_CPT, + CodeConstants.HCPCS: CodeConstants.URL_HCPCS, CodeConstants.HCPCSLEVELII: CodeConstants.URL_HCPCSLEVELII, CodeConstants.CVX: CodeConstants.URL_CVX, CodeConstants.LOINC: CodeConstants.URL_LOINC, From 5f4f018b5574b690dc3ec44fb2fc804ef2a2097e Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Thu, 7 Nov 2024 10:36:19 -0600 Subject: [PATCH 02/15] Added custom value sets for dysrhythmia CQM; some minor model updates to Condition and Medication --- canvas_sdk/v1/data/condition.py | 12 +++++++++++ canvas_sdk/v1/data/medication.py | 14 +++++++++--- canvas_sdk/value_set/custom.py | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/canvas_sdk/v1/data/condition.py b/canvas_sdk/v1/data/condition.py index d48b1de9..a8a22880 100644 --- a/canvas_sdk/v1/data/condition.py +++ b/canvas_sdk/v1/data/condition.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import TextChoices from canvas_sdk.v1.data.base import ( CommittableModelManager, @@ -9,6 +10,16 @@ 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.""" @@ -29,6 +40,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/custom.py b/canvas_sdk/value_set/custom.py index 6e87a624..dc53f772 100644 --- a/canvas_sdk/value_set/custom.py +++ b/canvas_sdk/value_set/custom.py @@ -13,3 +13,40 @@ class Hcc005v1AnnualWellnessVisit(ValueSet): "99387", "99397", } + + +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): + """Antiarrhythmic medications.""" + + 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 8aa87508fd29a534a2c6d721c17ec0076a6aa0f6 Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Thu, 7 Nov 2024 10:47:23 -0600 Subject: [PATCH 03/15] Added a method on ClinicalQualityMeasure to obtain the patient ID from an event target --- .../protocols/clinical_quality_measure.py | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/canvas_sdk/protocols/clinical_quality_measure.py b/canvas_sdk/protocols/clinical_quality_measure.py index 52781072..da4f9386 100644 --- a/canvas_sdk/protocols/clinical_quality_measure.py +++ b/canvas_sdk/protocols/clinical_quality_measure.py @@ -1,9 +1,13 @@ -from typing import Any +from typing import Any, cast import arrow +from django.db.models import Model +from canvas_sdk.events import EventType from canvas_sdk.protocols.base import BaseProtocol from canvas_sdk.protocols.timeframe import Timeframe +from canvas_sdk.v1.data.condition import Condition +from canvas_sdk.v1.data.medication import Medication class ClinicalQualityMeasure(BaseProtocol): @@ -26,6 +30,10 @@ class Meta: is_abstract: bool = False is_predictive: bool = False + def __init__(self, *args: Any, **kwargs: Any): + self.patient_id: str | None = None + super().__init__(*args, **kwargs) + @classmethod def _meta(cls) -> dict[str, Any]: """ @@ -58,3 +66,33 @@ def timeframe(self) -> Timeframe: def now(self) -> arrow.Arrow: """A convenience method for returning the current datetime.""" return arrow.utcnow() + + # TODO: This approach should be considered against the alternative of just including the patient + # ID in the event context, given that so many events will be patient-centric. + def set_patient_id(self) -> None: + """ + Set the patient ID based on the event target. + + This method will attempt to obtain the patient ID from the event target for supported event + types. It stores the patient ID on a member variable so that it can be referenced without + incurring more SQL queries. + """ + + def patient_id(model: type[Model]) -> str: + return cast( + str, + model.objects.select_related("patient") + .values_list("patient__id") + .get(id=self.event.target)[0], + ) + + # TODO: Add cases for ProtocolOverride + match self.event.type: + case EventType.CONDITION_CREATED | EventType.CONDITION_UPDATED: + self.patient_id = patient_id(Condition) + case EventType.MEDICATION_LIST_ITEM_CREATED | EventType.MEDICATION_LIST_ITEM_UPDATED: + self.patient_id = patient_id(Medication) + case _: + raise AssertionError( + f"Event type {self.event.type} not supported by 'patient_id_from_event'" + ) From 3ae3aed5c621fa610460b3874a67f90a4ce218df Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Thu, 7 Nov 2024 11:14:48 -0600 Subject: [PATCH 04/15] Refactor set_patient_id into a getter that will only fetch the patient ID once --- .../protocols/clinical_quality_measure.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/canvas_sdk/protocols/clinical_quality_measure.py b/canvas_sdk/protocols/clinical_quality_measure.py index da4f9386..860379f2 100644 --- a/canvas_sdk/protocols/clinical_quality_measure.py +++ b/canvas_sdk/protocols/clinical_quality_measure.py @@ -31,7 +31,7 @@ class Meta: is_predictive: bool = False def __init__(self, *args: Any, **kwargs: Any): - self.patient_id: str | None = None + self._patient_id: str | None = None super().__init__(*args, **kwargs) @classmethod @@ -69,9 +69,9 @@ def now(self) -> arrow.Arrow: # TODO: This approach should be considered against the alternative of just including the patient # ID in the event context, given that so many events will be patient-centric. - def set_patient_id(self) -> None: + def patient_id_from_target(self) -> str: """ - Set the patient ID based on the event target. + Get and return the patient ID from an event target. This method will attempt to obtain the patient ID from the event target for supported event types. It stores the patient ID on a member variable so that it can be referenced without @@ -86,13 +86,18 @@ def patient_id(model: type[Model]) -> str: .get(id=self.event.target)[0], ) - # TODO: Add cases for ProtocolOverride - match self.event.type: - case EventType.CONDITION_CREATED | EventType.CONDITION_UPDATED: - self.patient_id = patient_id(Condition) - case EventType.MEDICATION_LIST_ITEM_CREATED | EventType.MEDICATION_LIST_ITEM_UPDATED: - self.patient_id = patient_id(Medication) - case _: - raise AssertionError( - f"Event type {self.event.type} not supported by 'patient_id_from_event'" - ) + if not self._patient_id: + # TODO: Add cases for ProtocolOverride + match self.event.type: + case EventType.CONDITION_CREATED | EventType.CONDITION_UPDATED: + self._patient_id = patient_id(Condition) + case ( + EventType.MEDICATION_LIST_ITEM_CREATED | EventType.MEDICATION_LIST_ITEM_UPDATED + ): + self._patient_id = patient_id(Medication) + case _: + raise AssertionError( + f"Event type {self.event.type} not supported by 'patient_id_from_event'" + ) + + return self._patient_id From 0ea79d62e519d818af4e4c28f655621dea65f7b3 Mon Sep 17 00:00:00 2001 From: Christopher Sande Date: Thu, 7 Nov 2024 15:38:10 -0600 Subject: [PATCH 05/15] Added django.db.models as an allowed module --- plugin_runner/sandbox.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin_runner/sandbox.py b/plugin_runner/sandbox.py index 19c58622..8f6aec76 100644 --- a/plugin_runner/sandbox.py +++ b/plugin_runner/sandbox.py @@ -48,6 +48,7 @@ "contextlib", "datetime", "dateutil", + "django.db.models", "enum", "functools", "hashlib", From 97f86efb37920d18cf4e400e3dd5793518a0e9cb Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Wed, 13 Nov 2024 14:52:59 -0800 Subject: [PATCH 06/15] Adds .choices to model choices. --- canvas_sdk/v1/data/billing.py | 2 +- canvas_sdk/v1/data/note.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/canvas_sdk/v1/data/billing.py b/canvas_sdk/v1/data/billing.py index 526dc5d0..c0a49b7b 100644 --- a/canvas_sdk/v1/data/billing.py +++ b/canvas_sdk/v1/data/billing.py @@ -53,4 +53,4 @@ class Meta: units = models.IntegerField() command_type = models.CharField() command_id = models.IntegerField() - status = models.CharField(choices=BillingLineItemStatus) + status = models.CharField(choices=BillingLineItemStatus.choices) diff --git a/canvas_sdk/v1/data/note.py b/canvas_sdk/v1/data/note.py index bbc91305..5fe3855a 100644 --- a/canvas_sdk/v1/data/note.py +++ b/canvas_sdk/v1/data/note.py @@ -83,15 +83,15 @@ class Meta: user_selected = models.BooleanField() name = models.CharField() icon = models.CharField() - category = models.CharField(choices=NoteTypeCategories) + category = models.CharField(choices=NoteTypeCategories.choices) rank = models.PositiveIntegerField() is_default_appointment_type = models.BooleanField() is_scheduleable = models.BooleanField() is_telehealth = models.BooleanField() is_billable = models.BooleanField() defer_place_of_service_to_practice_location = models.BooleanField() - available_places_of_service = ArrayField(models.CharField(choices=PracticeLocationPOS)) - default_place_of_service = models.CharField(choices=PracticeLocationPOS) + available_places_of_service = ArrayField(models.CharField(choices=PracticeLocationPOS.choices)) + default_place_of_service = models.CharField(choices=PracticeLocationPOS.choices) is_system_managed = models.BooleanField() is_visible = models.BooleanField() is_active = models.BooleanField() @@ -133,7 +133,7 @@ class Meta: modified = models.DateTimeField() patient = models.ForeignKey(Patient, on_delete=models.DO_NOTHING) # provider = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="notes") - note_type = models.CharField(choices=NoteType, null=True) + note_type = models.CharField(choices=NoteType.choices, null=True) note_type_version = models.ForeignKey( "NoteType", on_delete=models.DO_NOTHING, related_name="notes" ) From 71fbb7535f05f79817a3d1db1810adbe6c21816d Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Fri, 15 Nov 2024 13:40:34 -0800 Subject: [PATCH 07/15] feat: additions for diabetes mellitus protocol to plugin conversion (#193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kristen ONeill <91080969+kristenoneill@users.noreply.github.com> Co-authored-by: Christopher Sande Co-authored-by: José Magalhães Co-authored-by: Michela Iannaccone Co-authored-by: Kristen ONeill <91080969+kristenoneill@users.noreply.github.com> --- canvas_sdk/v1/data/patient.py | 2 +- canvas_sdk/value_set/custom.py | 966 ++++++++++++++++++++++++++++++++- plugin_runner/sandbox.py | 1 + 3 files changed, 951 insertions(+), 18 deletions(-) diff --git a/canvas_sdk/v1/data/patient.py b/canvas_sdk/v1/data/patient.py index 09d7414f..e5c753cc 100644 --- a/canvas_sdk/v1/data/patient.py +++ b/canvas_sdk/v1/data/patient.py @@ -57,7 +57,7 @@ def __str__(self) -> str: def age_at(self, time: arrow.Arrow) -> float: """Given a datetime, returns what the patient's age would be at that datetime.""" - age = 0 + age = float(0) birth_date = arrow.get(self.birth_date) if birth_date.date() < time.date(): age = time.datetime.year - birth_date.datetime.year diff --git a/canvas_sdk/value_set/custom.py b/canvas_sdk/value_set/custom.py index dc53f772..9e6ac532 100644 --- a/canvas_sdk/value_set/custom.py +++ b/canvas_sdk/value_set/custom.py @@ -22,10 +22,47 @@ class DysrhythmiaClassConditionSuspect(ValueSet): 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" + "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", } @@ -36,17 +73,912 @@ class Antiarrhythmics(ValueSet): 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" + "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 DiabetesWithoutComplication(ValueSet): + """Diabetes Without Complication.""" + + VALUE_SET_NAME = "Diabetes Without Complication" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-16" + + ICD10CM = { + "E119", + } + + +class DiabetesEyeConditionSuspect(ValueSet): + """Diabetes Eye Condition suspect.""" + + VALUE_SET_NAME = "Diabetes Eye Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-16" + + ICD10CM = { + "H28", + "H36", + } + + +class DiabetesEyeClassConditionSuspect(ValueSet): + """Diabetes Eye Class Condition suspect.""" + + VALUE_SET_NAME = "Diabetes Eye Class Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-16" + + ICD10CM = { + "H3500", + "H35011", + "H35012", + "H35013", + "H35019", + "H35021", + "H35022", + "H35023", + "H35029", + "H35031", + "H35032", + "H35033", + "H35039", + "H35041", + "H35042", + "H35043", + "H35049", + "H35051", + "H35052", + "H35053", + "H35059", + "H35061", + "H35062", + "H35063", + "H35069", + "H35071", + "H35072", + "H35073", + "H35079", + "H3509", + "H35101", + "H35102", + "H35103", + "H35109", + "H35111", + "H35112", + "H35113", + "H35119", + "H35121", + "H35122", + "H35123", + "H35129", + "H35131", + "H35132", + "H35133", + "H35139", + "H35141", + "H35142", + "H35143", + "H35149", + "H35151", + "H35152", + "H35153", + "H35159", + "H35161", + "H35162", + "H35163", + "H35169", + "H35171", + "H35172", + "H35173", + "H35179", + "H3520", + "H3521", + "H3522", + "H3523", + "H3530", + "H3531", + "H353110", + "H353111", + "H353112", + "H353113", + "H353114", + "H353120", + "H353121", + "H353122", + "H353123", + "H353124", + "H353130", + "H353131", + "H353132", + "H353133", + "H353134", + "H353190", + "H353191", + "H353192", + "H353193", + "H353194", + "H3532", + "H353210", + "H353211", + "H353212", + "H353213", + "H353220", + "H353221", + "H353222", + "H353223", + "H353230", + "H353231", + "H353232", + "H353233", + "H353290", + "H353291", + "H353292", + "H353293", + "H3533", + "H35341", + "H35342", + "H35343", + "H35349", + "H35351", + "H35352", + "H35353", + "H35359", + "H35361", + "H35362", + "H35363", + "H35369", + "H35371", + "H35372", + "H35373", + "H35379", + "H35381", + "H35382", + "H35383", + "H35389", + "H3540", + "H35411", + "H35412", + "H35413", + "H35419", + "H35421", + "H35422", + "H35423", + "H35429", + "H35431", + "H35432", + "H35433", + "H35439", + "H35441", + "H35442", + "H35443", + "H35449", + "H35451", + "H35452", + "H35453", + "H35459", + "H35461", + "H35462", + "H35463", + "H35469", + "H3550", + "H3551", + "H3552", + "H3553", + "H3554", + "H3560", + "H3561", + "H3562", + "H3563", + "H3570", + "H35711", + "H35712", + "H35713", + "H35719", + "H35721", + "H35722", + "H35723", + "H35729", + "H35731", + "H35732", + "H35733", + "H35739", + "H3581", + "H3582", + "H3589", + "H359", + } + + +class DiabetesNeurologicConditionSuspect(ValueSet): + """Diabetes Neurologic Condition suspect.""" + + VALUE_SET_NAME = "Diabetes Neurologic Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-16" + + ICD10CM = { + "G63", + "G737", + "G53", + } + + +class DiabetesRenalConditionSuspect(ValueSet): + """Diabetes Renal Condition suspect.""" + + VALUE_SET_NAME = "Diabetes Renal Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-16" + + ICD10CM = { + "N181", + "N182", + "N183", + "N184", + "N185", + "N186", + "N189", + } + + +class DiabetesCirculatoryClassConditionSuspect(ValueSet): + """Diabetes Circulatory Class Condition suspect.""" + + VALUE_SET_NAME = "Diabetes Circulatory Class Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-16" + + ICD10CM = { + "I7300", + "I7301", + "I731", + "I7381", + "I7389", + "I739", + "I700", + "I701", + "I70201", + "I70202", + "I70203", + "I70208", + "I70209", + "I70211", + "I70212", + "I70213", + "I70218", + "I70219", + "I70221", + "I70222", + "I70223", + "I70228", + "I70229", + "I70231", + "I70232", + "I70233", + "I70234", + "I70235", + "I70238", + "I70239", + "I70241", + "I70242", + "I70243", + "I70244", + "I70245", + "I70248", + "I70249", + "I7025", + "I70261", + "I70262", + "I70263", + "I70268", + "I70269", + "I70291", + "I70292", + "I70293", + "I70298", + "I70299", + "I70301", + "I70302", + "I70303", + "I70308", + "I70309", + "I70311", + "I70312", + "I70313", + "I70318", + "I70319", + "I70321", + "I70322", + "I70323", + "I70328", + "I70329", + "I70331", + "I70332", + "I70333", + "I70334", + "I70335", + "I70338", + "I70339", + "I70341", + "I70342", + "I70343", + "I70344", + "I70345", + "I70348", + "I70349", + "I7035", + "I70361", + "I70362", + "I70363", + "I70368", + "I70369", + "I70391", + "I70392", + "I70393", + "I70398", + "I70399", + "I70401", + "I70402", + "I70403", + "I70408", + "I70409", + "I70411", + "I70412", + "I70413", + "I70418", + "I70419", + "I70421", + "I70422", + "I70423", + "I70428", + "I70429", + "I70431", + "I70432", + "I70433", + "I70434", + "I70435", + "I70438", + "I70439", + "I70441", + "I70442", + "I70443", + "I70444", + "I70445", + "I70448", + "I70449", + "I7045", + "I70461", + "I70462", + "I70463", + "I70468", + "I70469", + "I70491", + "I70492", + "I70493", + "I70498", + "I70499", + "I70501", + "I70502", + "I70503", + "I70508", + "I70509", + "I70511", + "I70512", + "I70513", + "I70518", + "I70519", + "I70521", + "I70522", + "I70523", + "I70528", + "I70529", + "I70531", + "I70532", + "I70533", + "I70534", + "I70535", + "I70538", + "I70539", + "I70541", + "I70542", + "I70543", + "I70544", + "I70545", + "I70548", + "I70549", + "I7055", + "I70561", + "I70562", + "I70563", + "I70568", + "I70569", + "I70591", + "I70592", + "I70593", + "I70598", + "I70599", + "I70601", + "I70602", + "I70603", + "I70608", + "I70609", + "I70611", + "I70612", + "I70613", + "I70618", + "I70619", + "I70621", + "I70622", + "I70623", + "I70628", + "I70629", + "I70631", + "I70632", + "I70633", + "I70634", + "I70635", + "I70638", + "I70639", + "I70641", + "I70642", + "I70643", + "I70644", + "I70645", + "I70648", + "I70649", + "I7065", + "I70661", + "I70662", + "I70663", + "I70668", + "I70669", + "I70691", + "I70692", + "I70693", + "I70698", + "I70699", + "I70701", + "I70702", + "I70703", + "I70708", + "I70709", + "I70711", + "I70712", + "I70713", + "I70718", + "I70719", + "I70721", + "I70722", + "I70723", + "I70728", + "I70729", + "I70731", + "I70732", + "I70733", + "I70734", + "I70735", + "I70738", + "I70739", + "I70741", + "I70742", + "I70743", + "I70744", + "I70745", + "I70748", + "I70749", + "I7075", + "I70761", + "I70762", + "I70763", + "I70768", + "I70769", + "I70791", + "I70792", + "I70793", + "I70798", + "I70799", + "I708", + "I7090", + "I7091", + "I7092", + "I7100", + "I7101", + "I71010", + "I71011", + "I71012", + "I71019", + "I7102", + "I7103", + "I711", + "I7110", + "I7111", + "I7112", + "I7113", + "I712", + "I7120", + "I7121", + "I7122", + "I7123", + "I713", + "I7130", + "I7131", + "I7132", + "I7133", + "I714", + "I7140", + "I7141", + "I7142", + "I7143", + "I715", + "I7150", + "I7151", + "I7152", + "I716", + "I7160", + "I7161", + "I7162", + "I718", + "I719", + "I790", + "I791", + "I798", + } + + +class DiabetesOtherClassConditionSuspect(ValueSet): + """Diabetes Other Class Condition suspect.""" + + VALUE_SET_NAME = "Diabetes Other Class Condition suspect" + EXPANSION_VERSION = "CanvasHCC Update 2018-10-16" + + ICD10CM = { + "M1460", + "M14611", + "M14612", + "M14619", + "M14621", + "M14622", + "M14629", + "M14631", + "M14632", + "M14639", + "M14641", + "M14642", + "M14649", + "M14651", + "M14652", + "M14659", + "M14661", + "M14662", + "M14669", + "M14671", + "M14672", + "M14679", + "M1468", + "M1469", + "M1480", + "M14811", + "M14812", + "M14819", + "M14821", + "M14822", + "M14829", + "M14831", + "M14832", + "M14839", + "M14841", + "M14842", + "M14849", + "M14851", + "M14852", + "M14859", + "M14861", + "M14862", + "M14869", + "M14871", + "M14872", + "M14879", + "M1488", + "M1489", + "L97101", + "L97102", + "L97103", + "L97104", + "L97105", + "L97106", + "L97108", + "L97109", + "L97111", + "L97112", + "L97113", + "L97114", + "L97115", + "L97116", + "L97118", + "L97119", + "L97121", + "L97122", + "L97123", + "L97124", + "L97125", + "L97126", + "L97128", + "L97129", + "L97201", + "L97202", + "L97203", + "L97204", + "L97205", + "L97206", + "L97208", + "L97209", + "L97211", + "L97212", + "L97213", + "L97214", + "L97215", + "L97216", + "L97218", + "L97219", + "L97221", + "L97222", + "L97223", + "L97224", + "L97225", + "L97226", + "L97228", + "L97229", + "L97301", + "L97302", + "L97303", + "L97304", + "L97305", + "L97306", + "L97308", + "L97309", + "L97311", + "L97312", + "L97313", + "L97314", + "L97315", + "L97316", + "L97318", + "L97319", + "L97321", + "L97322", + "L97323", + "L97324", + "L97325", + "L97326", + "L97328", + "L97329", + "L97401", + "L97402", + "L97403", + "L97404", + "L97405", + "L97406", + "L97408", + "L97409", + "L97411", + "L97412", + "L97413", + "L97414", + "L97415", + "L97416", + "L97418", + "L97419", + "L97421", + "L97422", + "L97423", + "L97424", + "L97425", + "L97426", + "L97428", + "L97429", + "L97501", + "L97502", + "L97503", + "L97504", + "L97505", + "L97506", + "L97508", + "L97509", + "L97511", + "L97512", + "L97513", + "L97514", + "L97515", + "L97516", + "L97518", + "L97519", + "L97521", + "L97522", + "L97523", + "L97524", + "L97525", + "L97526", + "L97528", + "L97529", + "L97801", + "L97802", + "L97803", + "L97804", + "L97805", + "L97806", + "L97808", + "L97809", + "L97811", + "L97812", + "L97813", + "L97814", + "L97815", + "L97816", + "L97818", + "L97819", + "L97821", + "L97822", + "L97823", + "L97824", + "L97825", + "L97826", + "L97828", + "L97829", + "L97901", + "L97902", + "L97903", + "L97904", + "L97905", + "L97906", + "L97908", + "L97909", + "L97911", + "L97912", + "L97913", + "L97914", + "L97915", + "L97916", + "L97918", + "L97919", + "L97921", + "L97922", + "L97923", + "L97924", + "L97925", + "L97926", + "L97928", + "L97929", + "L98411", + "L98412", + "L98413", + "L98414", + "L98415", + "L98416", + "L98418", + "L98419", + "L98421", + "L98422", + "L98423", + "L98424", + "L98425", + "L98426", + "L98428", + "L98429", + "L98491", + "L98492", + "L98493", + "L98494", + "L98495", + "L98496", + "L98498", + "L98499", } diff --git a/plugin_runner/sandbox.py b/plugin_runner/sandbox.py index 8f6aec76..d0b62789 100644 --- a/plugin_runner/sandbox.py +++ b/plugin_runner/sandbox.py @@ -49,6 +49,7 @@ "datetime", "dateutil", "django.db.models", + "django.utils.functional", "enum", "functools", "hashlib", From e06267d9b6d96080dba1019831986960820a0c35 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Tue, 19 Nov 2024 14:02:35 -0800 Subject: [PATCH 08/15] Adds now property to __init__ method. --- canvas_sdk/protocols/clinical_quality_measure.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/canvas_sdk/protocols/clinical_quality_measure.py b/canvas_sdk/protocols/clinical_quality_measure.py index 860379f2..bfbfcef9 100644 --- a/canvas_sdk/protocols/clinical_quality_measure.py +++ b/canvas_sdk/protocols/clinical_quality_measure.py @@ -32,6 +32,7 @@ class Meta: def __init__(self, *args: Any, **kwargs: Any): self._patient_id: str | None = None + self.now = arrow.utcnow() super().__init__(*args, **kwargs) @classmethod @@ -62,11 +63,6 @@ def timeframe(self) -> Timeframe: end = self.now return Timeframe(start=end.shift(years=-1), end=end) - @property - def now(self) -> arrow.Arrow: - """A convenience method for returning the current datetime.""" - return arrow.utcnow() - # TODO: This approach should be considered against the alternative of just including the patient # ID in the event context, given that so many events will be patient-centric. def patient_id_from_target(self) -> str: From a6147357aea6dc2b2bd79c4b527a83fb86b408e9 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Tue, 19 Nov 2024 14:10:22 -0800 Subject: [PATCH 09/15] Moves choice classes outside of models. --- canvas_sdk/v1/data/billing.py | 11 ++- canvas_sdk/v1/data/note.py | 163 ++++++++++++++++++---------------- 2 files changed, 93 insertions(+), 81 deletions(-) diff --git a/canvas_sdk/v1/data/billing.py b/canvas_sdk/v1/data/billing.py index c0a49b7b..e8e6a69d 100644 --- a/canvas_sdk/v1/data/billing.py +++ b/canvas_sdk/v1/data/billing.py @@ -24,13 +24,16 @@ def find(self, value_set: Type["ValueSet"]) -> models.QuerySet: return self.filter(cpt__in=values_dict.get(CodeConstants.HCPCS, [])) +class BillingLineItemStatus(models.TextChoices): + """Billing line item status.""" + + ACTIVE = "active", "Active" + REMOVED = "removed", "Removed" + + class BillingLineItem(models.Model): """BillingLineItem.""" - class BillingLineItemStatus(models.TextChoices): - ACTIVE = "active", "Active" - REMOVED = "removed", "Removed" - class Meta: managed = False app_label = "canvas_sdk" diff --git a/canvas_sdk/v1/data/note.py b/canvas_sdk/v1/data/note.py index 5fe3855a..194ad3ce 100644 --- a/canvas_sdk/v1/data/note.py +++ b/canvas_sdk/v1/data/note.py @@ -7,67 +7,94 @@ from canvas_sdk.v1.data.user import CanvasUser +class NoteTypeCategories(models.TextChoices): + """Note type categories.""" + + MESSAGE = "message", "Message" + LETTER = "letter", "Letter" + INPATIENT = "inpatient", "Inpatient Visit Note" + REVIEW = "review", "Chart Review Note" + ENCOUNTER = "encounter", "Encounter Note" + APPOINTMENT = "appointment", "Appointment Note" + TASK = "task", "Task" + DATA = "data", "Data" + CCDA = "ccda", "C-CDA" + SCHEDULE_EVENT = "schedule_event", "Schedule Event" + + +class PracticeLocationPOS(models.TextChoices): + """Practice Location POS.""" + + PHARMACY = "01", "Pharmacy" + TELEHEALTH = "02", "Telehealth" + SCHOOL = "03", "Education Facility" + HOMELESS_SHELTER = "04", "Homeless Shelter" + PRISON = "09", "Prison" + TELEHEALTH_IN_PATIENT_HOME = "10", "Telehealth in Patient's Home" + OFFICE = "11", "Office" + HOME = "12", "Home" + ASSISTED_LIVING = "13", "Asssisted Living Facility" + GROUP_HOME = "14", "Group Home" + MOBILE = "15", "Mobile Unit" + WALK_IN_RETAIL = "17", "Walk-In Retail Health Clinic" + OFF_CAMPUS_OUTPATIENT_HOSPITAL = "19", "Off-Campus Outpatient Hospital" + URGENT_CARE = "20", "Urgent Care Facility" + INPATIENT_HOSPITAL = "21", "Inpatient Hospital" + ON_CAMPUS_OUTPATIENT_HOSPITAL = "22", "On-Campus Outpatient Hospital" + ER_HOSPITAL = "23", "Emergency Room Hospital" + AMBULATORY_SURGERY_CENTER = "24", "Ambulatory Surgery Center" + BIRTHING_CENTER = "25", "Birthing Center" + MILITARY_FACILITY = "26", "Military Treatment Facility" + STREET = "27", "Outreach Site / Street" + SNF = "31", "Skilled Nursing Facility" + NURSING = "32", "Nursing Facility" + CUSTODIAL = "33", "Custodial Care Facility" + HOSPICE = "34", "Hospice" + AMBULANCE_LAND = "41", "Ambulance Land" + AMBULANCE_AIR_WATER = "42", "Ambulance Air or Water" + INDEPENDENT_CLINIC = "49", "Independent Clinic" + FQHC = "50", "Federally Qualified Health Center" + PSYCH = "51", "Inpatient Psychiatric Facility" + PSYCH_PARTIAL = "52", "Inpatient Psychiatric Facility - Partial Hospitalization" + MENTAL_HEALTH_CENTER = "53", "Community Mental Health Center" + INTERMEDIATE_MENTAL = "54", "Intermediate Care Facility for Mentally Retarded" + SUBSTANCE_RESIDENTIAL = "55", "Residential Substance Abuse Treatment Facility" + PSYCH_RESIDENTIAL = "56", "Psychiatric Residential Treatment Center" + SUBSTANCE_NON_RESIDENTIAL = "57", "Non-Residential Substance Abuse Treatment Facility" + MASS_IMMUNIZATION = "60", "Mass Immunization Center" + INPATIENT_REHAB = "61", "Inpatient Rehabilitation Facility" + OUTPATIENT_REHAB = "62", "Outpatient Rehabilitation Facility" + ESRD = "65", "End-Stage Renal Disease Treatment Facility" + PUBLIC_CLINIC = "71", "State or Local Public Health Clinic" + RURAL_CLINIC = "72", "Rural Health Clinic" + INDEPENDENT_LAB = "81", "Independent Laboratory" + OTHER = "99", "Other Place of Service" + + +class NoteTypes(models.TextChoices): + """Note types.""" + + MESSAGE = "message", "Message" + LETTER = "letter", "Letter" + INPATIENT = "inpatient", "Inpatient Visit Note" + REVIEW = "review", "Chart Review Note" + VOICE = "voice", "Phone Call Note" + VIDEO = "video", "Video Call Note" + OFFICE = "office", "Office Visit Note" + LAB = "lab", "Lab Visit Note" + HOME = "home", "Home Visit Note" + GROUP = "group", "Group Visit Note" + APPOINTMENT = "appointment", "Appointment Note" + OFFSITE = "offsite", "Other Offsite Visit Note" + SEARCH = "search", "Search" + TASK = "task", "Task" + DATA = "data", "Data" + CCDA = "ccda", "C-CDA Import" + + class NoteType(models.Model): """NoteType.""" - class NoteTypeCategories(models.TextChoices): - MESSAGE = "message", "Message" - LETTER = "letter", "Letter" - INPATIENT = "inpatient", "Inpatient Visit Note" - REVIEW = "review", "Chart Review Note" - ENCOUNTER = "encounter", "Encounter Note" - APPOINTMENT = "appointment", "Appointment Note" - TASK = "task", "Task" - DATA = "data", "Data" - CCDA = "ccda", "C-CDA" - SCHEDULE_EVENT = "schedule_event", "Schedule Event" - - class PracticeLocationPOS(models.TextChoices): - PHARMACY = "01", "Pharmacy" - TELEHEALTH = "02", "Telehealth" - SCHOOL = "03", "Education Facility" - HOMELESS_SHELTER = "04", "Homeless Shelter" - PRISON = "09", "Prison" - TELEHEALTH_IN_PATIENT_HOME = "10", "Telehealth in Patient's Home" - OFFICE = "11", "Office" - HOME = "12", "Home" - ASSISTED_LIVING = "13", "Asssisted Living Facility" - GROUP_HOME = "14", "Group Home" - MOBILE = "15", "Mobile Unit" - WALK_IN_RETAIL = "17", "Walk-In Retail Health Clinic" - OFF_CAMPUS_OUTPATIENT_HOSPITAL = "19", "Off-Campus Outpatient Hospital" - URGENT_CARE = "20", "Urgent Care Facility" - INPATIENT_HOSPITAL = "21", "Inpatient Hospital" - ON_CAMPUS_OUTPATIENT_HOSPITAL = "22", "On-Campus Outpatient Hospital" - ER_HOSPITAL = "23", "Emergency Room Hospital" - AMBULATORY_SURGERY_CENTER = "24", "Ambulatory Surgery Center" - BIRTHING_CENTER = "25", "Birthing Center" - MILITARY_FACILITY = "26", "Military Treatment Facility" - STREET = "27", "Outreach Site / Street" - SNF = "31", "Skilled Nursing Facility" - NURSING = "32", "Nursing Facility" - CUSTODIAL = "33", "Custodial Care Facility" - HOSPICE = "34", "Hospice" - AMBULANCE_LAND = "41", "Ambulance Land" - AMBULANCE_AIR_WATER = "42", "Ambulance Air or Water" - INDEPENDENT_CLINIC = "49", "Independent Clinic" - FQHC = "50", "Federally Qualified Health Center" - PSYCH = "51", "Inpatient Psychiatric Facility" - PSYCH_PARTIAL = "52", "Inpatient Psychiatric Facility - Partial Hospitalization" - MENTAL_HEALTH_CENTER = "53", "Community Mental Health Center" - INTERMEDIATE_MENTAL = "54", "Intermediate Care Facility for Mentally Retarded" - SUBSTANCE_RESIDENTIAL = "55", "Residential Substance Abuse Treatment Facility" - PSYCH_RESIDENTIAL = "56", "Psychiatric Residential Treatment Center" - SUBSTANCE_NON_RESIDENTIAL = "57", "Non-Residential Substance Abuse Treatment Facility" - MASS_IMMUNIZATION = "60", "Mass Immunization Center" - INPATIENT_REHAB = "61", "Inpatient Rehabilitation Facility" - OUTPATIENT_REHAB = "62", "Outpatient Rehabilitation Facility" - ESRD = "65", "End-Stage Renal Disease Treatment Facility" - PUBLIC_CLINIC = "71", "State or Local Public Health Clinic" - RURAL_CLINIC = "72", "Rural Health Clinic" - INDEPENDENT_LAB = "81", "Independent Laboratory" - OTHER = "99", "Other Place of Service" - class Meta: managed = False app_label = "canvas_sdk" @@ -104,24 +131,6 @@ class Meta: class Note(models.Model): """Note.""" - class NoteType(models.TextChoices): - MESSAGE = "message", "Message" - LETTER = "letter", "Letter" - INPATIENT = "inpatient", "Inpatient Visit Note" - REVIEW = "review", "Chart Review Note" - VOICE = "voice", "Phone Call Note" - VIDEO = "video", "Video Call Note" - OFFICE = "office", "Office Visit Note" - LAB = "lab", "Lab Visit Note" - HOME = "home", "Home Visit Note" - GROUP = "group", "Group Visit Note" - APPOINTMENT = "appointment", "Appointment Note" - OFFSITE = "offsite", "Other Offsite Visit Note" - SEARCH = "search", "Search" - TASK = "task", "Task" - DATA = "data", "Data" - CCDA = "ccda", "C-CDA Import" - class Meta: managed = False app_label = "canvas_sdk" @@ -133,7 +142,7 @@ class Meta: modified = models.DateTimeField() patient = models.ForeignKey(Patient, on_delete=models.DO_NOTHING) # provider = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="notes") - note_type = models.CharField(choices=NoteType.choices, null=True) + note_type = models.CharField(choices=NoteTypes.choices, null=True) note_type_version = models.ForeignKey( "NoteType", on_delete=models.DO_NOTHING, related_name="notes" ) From 7af75787c64f3b4ea653e1a0daab525464ffbb8e Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Tue, 19 Nov 2024 15:22:44 -0800 Subject: [PATCH 10/15] Changes inherited queryset classes to mixins and adds mypy/typing protocols. --- canvas_sdk/v1/data/base.py | 77 +++++++++++++++++++++++++++++-- canvas_sdk/v1/data/billing.py | 4 +- canvas_sdk/value_set/value_set.py | 2 +- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index d66fe4e5..d50a2e09 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -1,5 +1,10 @@ +from abc import abstractmethod from collections.abc import Container +<<<<<<< HEAD from typing import TYPE_CHECKING, Self, Type, cast +======= +from typing import TYPE_CHECKING, Any, Dict, Protocol, Type, cast +>>>>>>> 829cecf (Changes inherited queryset classes to mixins and adds mypy/typing protocols.) from django.db import models from django.db.models import Q @@ -30,8 +35,43 @@ def for_patient(self, patient_id: str) -> "Self": return self.filter(patient__id=patient_id) +<<<<<<< HEAD class ValueSetLookupQuerySet(CommittableQuerySet): """A QuerySet that can filter objects based on a ValueSet.""" +======= +class BaseQuerySet(models.QuerySet): + """A base QuerySet inherited from Django's model.Queryset.""" + + pass + + +class QuerySetProtocol(Protocol): + """A typing protocol for use in mixins into models.QuerySet-inherited classes.""" + + def filter(self, *args: Any, **kwargs: Any) -> models.QuerySet[Any]: + """Django's models.QuerySet filter method.""" + ... + + +class ValueSetLookupQuerySetProtocol(QuerySetProtocol): + """A typing protocol for use in mixins using value set lookup methods.""" + + @staticmethod + @abstractmethod + def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: + """A protocol method for defining codings.""" + ... + + @staticmethod + @abstractmethod + def q_object(system: str, codes: Container[str]) -> Q: + """A protocol method for defining Q objects for value set lookups.""" + ... + + +class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol): + """A QuerySet mixin that can filter objects based on a ValueSet.""" +>>>>>>> 829cecf (Changes inherited queryset classes to mixins and adds mypy/typing protocols.) def find(self, value_set: Type["ValueSet"]) -> "Self": """ @@ -55,7 +95,7 @@ def find(self, value_set: Type["ValueSet"]) -> "Self": @staticmethod def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: """Provide a sequence of tuples where each tuple is a code system URL and a set of codes.""" - values_dict = value_set.values + values_dict = cast(Dict, value_set.values) return cast( tuple[tuple[str, set[str]]], tuple( @@ -73,7 +113,7 @@ def q_object(system: str, codes: Container[str]) -> Q: return Q(codings__system=system, codings__code__in=codes) -class ValueSetLookupByNameQuerySet(ValueSetLookupQuerySet): +class ValueSetLookupByNameQuerySetMixin(ValueSetLookupQuerySetMixin): """ QuerySet for ValueSet lookups using code system name rather than URL. @@ -86,7 +126,7 @@ def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: """ Provide a sequence of tuples where each tuple is a code system name and a set of codes. """ - values_dict = value_set.values + values_dict = cast(Dict, value_set.values) return cast( tuple[tuple[str, set[str]]], tuple( @@ -97,7 +137,18 @@ def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: ) -class TimeframeLookupQuerySet(models.QuerySet): +class TimeframeLookupQuerySetProtocol(QuerySetProtocol): + """A typing protocol for use in TimeframeLookupQuerySetMixin.""" + + @property + @abstractmethod + def timeframe_filter_field(self) -> str: + """A protocol method for timeframe_filter_field.""" + + ... + + +class TimeframeLookupQuerySetMixin(TimeframeLookupQuerySetProtocol): """A class that adds queryset functionality to filter using timeframes.""" @property @@ -115,3 +166,21 @@ def within(self, timeframe: "Timeframe") -> models.QuerySet: ) } ) + + +class ValueSetLookupQuerySet(BaseQuerySet, ValueSetLookupQuerySetMixin): + """A class that includes methods for looking up value sets.""" + + pass + + +class ValueSetLookupByNameQuerySet(BaseQuerySet, ValueSetLookupByNameQuerySetMixin): + """A class that includes methods for looking up value sets by name.""" + + pass + + +class ValueSetTimeframeLookupQuerySet(ValueSetLookupByNameQuerySet, TimeframeLookupQuerySetMixin): + """A class that includes methods for looking up value sets and using timeframes.""" + + pass diff --git a/canvas_sdk/v1/data/billing.py b/canvas_sdk/v1/data/billing.py index e8e6a69d..bc7f2763 100644 --- a/canvas_sdk/v1/data/billing.py +++ b/canvas_sdk/v1/data/billing.py @@ -2,7 +2,7 @@ from django.db import models -from canvas_sdk.v1.data.base import TimeframeLookupQuerySet, ValueSetLookupQuerySet +from canvas_sdk.v1.data.base import ValueSetTimeframeLookupQuerySet from canvas_sdk.v1.data.note import Note from canvas_sdk.v1.data.patient import Patient from canvas_sdk.value_set.value_set import CodeConstants @@ -11,7 +11,7 @@ from canvas_sdk.value_set.value_set import ValueSet -class BillingLineItemQuerySet(ValueSetLookupQuerySet, TimeframeLookupQuerySet): +class BillingLineItemQuerySet(ValueSetTimeframeLookupQuerySet): """A class that adds functionality to filter BillingLineItem objects.""" def find(self, value_set: Type["ValueSet"]) -> models.QuerySet: diff --git a/canvas_sdk/value_set/value_set.py b/canvas_sdk/value_set/value_set.py index 12b4af40..0bfb3185 100644 --- a/canvas_sdk/value_set/value_set.py +++ b/canvas_sdk/value_set/value_set.py @@ -103,7 +103,7 @@ class ValueSet(CodeConstantsURLMappingMixin, metaclass=ValueSystems): """The Base class for a ValueSet.""" @classproperty - def values(cls) -> dict[str, set]: + def values(cls) -> dict[str, set[str]]: """A property that returns a dictionary of code systems and their associated values.""" return { system: getattr(cls, system) From 2058df40cdefb13b94b21b7a51e35f3f8dddc2f2 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Wed, 20 Nov 2024 13:23:11 -0800 Subject: [PATCH 11/15] Adds NotImplementedError to protocol class abstract methods. --- canvas_sdk/v1/data/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index d50a2e09..efc94600 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -60,13 +60,13 @@ class ValueSetLookupQuerySetProtocol(QuerySetProtocol): @abstractmethod def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: """A protocol method for defining codings.""" - ... + raise NotImplementedError @staticmethod @abstractmethod def q_object(system: str, codes: Container[str]) -> Q: """A protocol method for defining Q objects for value set lookups.""" - ... + raise NotImplementedError class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol): @@ -144,8 +144,7 @@ class TimeframeLookupQuerySetProtocol(QuerySetProtocol): @abstractmethod def timeframe_filter_field(self) -> str: """A protocol method for timeframe_filter_field.""" - - ... + raise NotImplementedError class TimeframeLookupQuerySetMixin(TimeframeLookupQuerySetProtocol): From fed9963f620d8fc920179d952469bc9e201116ef Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Wed, 20 Nov 2024 13:25:40 -0800 Subject: [PATCH 12/15] Changes base class in ValueSetTimeframeLookupQuerySet --- canvas_sdk/v1/data/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index efc94600..2dc165a9 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -179,7 +179,7 @@ class ValueSetLookupByNameQuerySet(BaseQuerySet, ValueSetLookupByNameQuerySetMix pass -class ValueSetTimeframeLookupQuerySet(ValueSetLookupByNameQuerySet, TimeframeLookupQuerySetMixin): +class ValueSetTimeframeLookupQuerySet(ValueSetLookupQuerySet, TimeframeLookupQuerySetMixin): """A class that includes methods for looking up value sets and using timeframes.""" pass From bf4e7b6685631ded866a8eda8f52d6dddb8525cc Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Mon, 25 Nov 2024 11:37:41 -0800 Subject: [PATCH 13/15] Fixes conflicts. --- canvas_sdk/v1/data/base.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index 2dc165a9..ccd2b0c1 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -1,10 +1,6 @@ from abc import abstractmethod from collections.abc import Container -<<<<<<< HEAD -from typing import TYPE_CHECKING, Self, Type, cast -======= from typing import TYPE_CHECKING, Any, Dict, Protocol, Type, cast ->>>>>>> 829cecf (Changes inherited queryset classes to mixins and adds mypy/typing protocols.) from django.db import models from django.db.models import Q @@ -35,10 +31,6 @@ def for_patient(self, patient_id: str) -> "Self": return self.filter(patient__id=patient_id) -<<<<<<< HEAD -class ValueSetLookupQuerySet(CommittableQuerySet): - """A QuerySet that can filter objects based on a ValueSet.""" -======= class BaseQuerySet(models.QuerySet): """A base QuerySet inherited from Django's model.Queryset.""" @@ -71,7 +63,6 @@ def q_object(system: str, codes: Container[str]) -> Q: class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol): """A QuerySet mixin that can filter objects based on a ValueSet.""" ->>>>>>> 829cecf (Changes inherited queryset classes to mixins and adds mypy/typing protocols.) def find(self, value_set: Type["ValueSet"]) -> "Self": """ From 97ef7a6750c4158c568d500dd219ccfd05dd27aa Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Mon, 25 Nov 2024 13:22:31 -0800 Subject: [PATCH 14/15] Mypy/typing changes. --- canvas_sdk/v1/data/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index ccd2b0c1..f1e4fd73 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -1,6 +1,6 @@ from abc import abstractmethod from collections.abc import Container -from typing import TYPE_CHECKING, Any, Dict, Protocol, Type, cast +from typing import TYPE_CHECKING, Any, Protocol, Self, Type, cast from django.db import models from django.db.models import Q @@ -64,7 +64,7 @@ def q_object(system: str, codes: Container[str]) -> Q: class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol): """A QuerySet mixin that can filter objects based on a ValueSet.""" - def find(self, value_set: Type["ValueSet"]) -> "Self": + def find(self, value_set: Type["ValueSet"]) -> models.QuerySet[Any]: """ Filters conditions, medications, etc. to those found in the inherited ValueSet class that is passed. @@ -86,7 +86,7 @@ def find(self, value_set: Type["ValueSet"]) -> "Self": @staticmethod def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: """Provide a sequence of tuples where each tuple is a code system URL and a set of codes.""" - values_dict = cast(Dict, value_set.values) + values_dict = cast(dict, value_set.values) return cast( tuple[tuple[str, set[str]]], tuple( @@ -117,7 +117,7 @@ def codings(value_set: Type["ValueSet"]) -> tuple[tuple[str, set[str]]]: """ Provide a sequence of tuples where each tuple is a code system name and a set of codes. """ - values_dict = cast(Dict, value_set.values) + values_dict = cast(dict, value_set.values) return cast( tuple[tuple[str, set[str]]], tuple( From e1c1fe9944f2a537673c088fe4484b4cfe693802 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Mon, 25 Nov 2024 15:19:50 -0800 Subject: [PATCH 15/15] Changes objects to _default_manager. --- canvas_sdk/protocols/clinical_quality_measure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canvas_sdk/protocols/clinical_quality_measure.py b/canvas_sdk/protocols/clinical_quality_measure.py index bfbfcef9..881218f3 100644 --- a/canvas_sdk/protocols/clinical_quality_measure.py +++ b/canvas_sdk/protocols/clinical_quality_measure.py @@ -77,7 +77,7 @@ def patient_id_from_target(self) -> str: def patient_id(model: type[Model]) -> str: return cast( str, - model.objects.select_related("patient") + model._default_manager.select_related("patient") .values_list("patient__id") .get(id=self.event.target)[0], )