From 83862c40cedf4326b98b86c7e1b01eba73d18ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Magalh=C3=A3es?= Date: Tue, 26 Nov 2024 18:51:37 +0000 Subject: [PATCH 1/4] feat: add mixins for CommittableQuerySet and PatientAssetQuerySet --- canvas_sdk/v1/data/base.py | 47 +++++++++++++++++++++------------ canvas_sdk/v1/data/condition.py | 20 +++++++++++--- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index 437770f7..1a60ba07 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, Protocol, Self, cast +from typing import TYPE_CHECKING, Any, Generic, Protocol, Type, TypeVar, cast from django.db import models from django.db.models import Q @@ -10,6 +10,9 @@ from canvas_sdk.value_set.value_set import ValueSet +CustomQuerySet = TypeVar("CustomQuerySet", bound=models.QuerySet) + + class CommittableModelManager(models.Manager): """A manager for commands that can be committed.""" @@ -19,18 +22,6 @@ def get_queryset(self) -> "CommittableQuerySet": return CommittableQuerySet(self.model, using=self._db).filter(deleted=False) -class CommittableQuerySet(models.QuerySet): - """A queryset for committable objects.""" - - def committed(self) -> "Self": - """Return a queryset that filters for objects that have been committed.""" - return self.filter(committer_id__isnull=False, entered_in_error_id__isnull=True) - - def for_patient(self, patient_id: str) -> "Self": - """Return a queryset that filters objects for a specific patient.""" - return self.filter(patient__id=patient_id) - - class BaseQuerySet(models.QuerySet): """A base QuerySet inherited from Django's model.Queryset.""" @@ -40,7 +31,7 @@ class BaseQuerySet(models.QuerySet): 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]: + def filter(self, *args: Any, **kwargs: Any) -> Any: """Django's models.QuerySet filter method.""" ... @@ -61,10 +52,26 @@ def q_object(system: str, codes: Container[str]) -> Q: raise NotImplementedError -class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol): +class CommittableQuerySetMixin(QuerySetProtocol, Generic[CustomQuerySet]): + """A queryset for committable objects.""" + + def committed(self) -> CustomQuerySet: + """Return a queryset that filters for objects that have been committed.""" + return self.filter(committer_id__isnull=False, entered_in_error_id__isnull=True) + + +class PatientAssetQuerySetMixin(QuerySetProtocol, Generic[CustomQuerySet]): + """A queryset for patient assets.""" + + def for_patient(self, patient_id: str) -> CustomQuerySet: + """Return a queryset that filters objects for a specific patient.""" + return self.filter(patient__id=patient_id) + + +class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol, Generic[CustomQuerySet]): """A QuerySet mixin that can filter objects based on a ValueSet.""" - def find(self, value_set: type["ValueSet"]) -> models.QuerySet[Any]: + def find(self, value_set: Type["ValueSet"]) -> CustomQuerySet: """ Filters conditions, medications, etc. to those found in the inherited ValueSet class that is passed. @@ -158,7 +165,13 @@ def within(self, timeframe: "Timeframe") -> models.QuerySet: ) -class ValueSetLookupQuerySet(CommittableQuerySet, ValueSetLookupQuerySetMixin): +class CommittableQuerySet(BaseQuerySet, CommittableQuerySetMixin): + """A queryset for committable objects.""" + + pass + + +class ValueSetLookupQuerySet(BaseQuerySet, ValueSetLookupQuerySetMixin): """A class that includes methods for looking up value sets.""" pass diff --git a/canvas_sdk/v1/data/condition.py b/canvas_sdk/v1/data/condition.py index a112757b..7608a0a8 100644 --- a/canvas_sdk/v1/data/condition.py +++ b/canvas_sdk/v1/data/condition.py @@ -1,7 +1,16 @@ +from typing import cast + from django.db import models from django.db.models import TextChoices -from canvas_sdk.v1.data.base import ValueSetLookupQuerySet +from canvas_sdk.v1.data.base import ( + BaseQuerySet, + CommittableQuerySetMixin, + PatientAssetQuerySetMixin, + ValueSetLookupQuerySetMixin, +) +from canvas_sdk.v1.data.patient import Patient +from canvas_sdk.v1.data.user import CanvasUser class ClinicalStatus(TextChoices): @@ -14,7 +23,12 @@ class ClinicalStatus(TextChoices): INVESTIGATIVE = "investigative", "investigative" -class ConditionQuerySet(ValueSetLookupQuerySet): +class ConditionQuerySet( + BaseQuerySet, + ValueSetLookupQuerySetMixin["ConditionQuerySet"], + CommittableQuerySetMixin["ConditionQuerySet"], + PatientAssetQuerySetMixin["ConditionQuerySet"], +): """ConditionQuerySet.""" pass @@ -27,7 +41,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_condition_001" - objects = ConditionQuerySet.as_manager() + objects = cast(ConditionQuerySet, ConditionQuerySet.as_manager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) From 681e524be41d29a8ed88e69abc33255647943756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Magalh=C3=A3es?= Date: Thu, 5 Dec 2024 21:42:35 +0000 Subject: [PATCH 2/4] refactor base manager --- canvas_sdk/v1/data/allergy_intolerance.py | 24 ++++++++++++++-- canvas_sdk/v1/data/base.py | 34 +++++++++++------------ canvas_sdk/v1/data/billing.py | 4 +-- canvas_sdk/v1/data/condition.py | 16 ++++++----- canvas_sdk/v1/data/lab.py | 29 +++++++++++++++++-- canvas_sdk/v1/data/medication.py | 16 +++++++++-- canvas_sdk/v1/data/observation.py | 18 ++++++++++-- canvas_sdk/v1/data/protocol_override.py | 20 +++++++++++-- canvas_sdk/v1/data/questionnaire.py | 17 ++++++++++-- 9 files changed, 137 insertions(+), 41 deletions(-) diff --git a/canvas_sdk/v1/data/allergy_intolerance.py b/canvas_sdk/v1/data/allergy_intolerance.py index 6595976f..2ad2d586 100644 --- a/canvas_sdk/v1/data/allergy_intolerance.py +++ b/canvas_sdk/v1/data/allergy_intolerance.py @@ -1,6 +1,26 @@ +from typing import cast + from django.db import models -from canvas_sdk.v1.data.base import CommittableModelManager, ValueSetLookupQuerySet +from canvas_sdk.v1.data.base import ( + BaseModelManager, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, + ValueSetLookupQuerySet, +) + + +class AllergyIntoleranceQuerySet( + ValueSetLookupQuerySet, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, +): + """AllergyIntoleranceQuerySet.""" + + pass + + +AllergyIntoleranceManager = BaseModelManager.from_queryset(AllergyIntoleranceQuerySet) class AllergyIntolerance(models.Model): @@ -10,7 +30,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_allergyintolerance_001" - objects = CommittableModelManager().from_queryset(ValueSetLookupQuerySet)() + objects = cast(AllergyIntoleranceQuerySet, AllergyIntoleranceManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/v1/data/base.py b/canvas_sdk/v1/data/base.py index 1a60ba07..15d38c2f 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, Generic, Protocol, Type, TypeVar, cast +from typing import TYPE_CHECKING, Any, Protocol, Self, cast from django.db import models from django.db.models import Q @@ -10,16 +10,12 @@ from canvas_sdk.value_set.value_set import ValueSet -CustomQuerySet = TypeVar("CustomQuerySet", bound=models.QuerySet) +class BaseModelManager(models.Manager): + """A base manager for models.""" - -class CommittableModelManager(models.Manager): - """A manager for commands that can be committed.""" - - def get_queryset(self) -> "CommittableQuerySet": + def get_queryset(self) -> models.QuerySet: """Return a queryset that filters out deleted objects.""" - # TODO: Should we just filter these out at the view level? - return CommittableQuerySet(self.model, using=self._db).filter(deleted=False) + return self.filter(deleted=False) class BaseQuerySet(models.QuerySet): @@ -31,10 +27,14 @@ class BaseQuerySet(models.QuerySet): class QuerySetProtocol(Protocol): """A typing protocol for use in mixins into models.QuerySet-inherited classes.""" - def filter(self, *args: Any, **kwargs: Any) -> Any: + def filter(self, *args: Any, **kwargs: Any) -> Self: """Django's models.QuerySet filter method.""" ... + def distinct(self) -> Self: + """Django's models.QuerySet distinct method.""" + ... + class ValueSetLookupQuerySetProtocol(QuerySetProtocol): """A typing protocol for use in mixins using value set lookup methods.""" @@ -52,26 +52,26 @@ def q_object(system: str, codes: Container[str]) -> Q: raise NotImplementedError -class CommittableQuerySetMixin(QuerySetProtocol, Generic[CustomQuerySet]): +class CommittableQuerySetMixin(QuerySetProtocol): """A queryset for committable objects.""" - def committed(self) -> CustomQuerySet: + def committed(self) -> Self: """Return a queryset that filters for objects that have been committed.""" return self.filter(committer_id__isnull=False, entered_in_error_id__isnull=True) -class PatientAssetQuerySetMixin(QuerySetProtocol, Generic[CustomQuerySet]): +class ForPatientQuerySetMixin(QuerySetProtocol): """A queryset for patient assets.""" - def for_patient(self, patient_id: str) -> CustomQuerySet: + def for_patient(self, patient_id: str) -> Self: """Return a queryset that filters objects for a specific patient.""" return self.filter(patient__id=patient_id) -class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol, Generic[CustomQuerySet]): +class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol): """A QuerySet mixin that can filter objects based on a ValueSet.""" - def find(self, value_set: Type["ValueSet"]) -> CustomQuerySet: + def find(self, value_set: type["ValueSet"]) -> Self: """ Filters conditions, medications, etc. to those found in the inherited ValueSet class that is passed. @@ -153,7 +153,7 @@ 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: + def within(self, timeframe: "Timeframe") -> Self: """A method to filter a queryset for datetimes within a timeframe.""" return self.filter( **{ diff --git a/canvas_sdk/v1/data/billing.py b/canvas_sdk/v1/data/billing.py index 88253084..f86de0c9 100644 --- a/canvas_sdk/v1/data/billing.py +++ b/canvas_sdk/v1/data/billing.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self from django.db import models @@ -12,7 +12,7 @@ class BillingLineItemQuerySet(ValueSetTimeframeLookupQuerySet): """A class that adds functionality to filter BillingLineItem objects.""" - def find(self, value_set: type["ValueSet"]) -> models.QuerySet: + def find(self, value_set: type["ValueSet"]) -> Self: """ This method is overridden to use for BillingLineItem CPT codes. The codes are saved as string values in the BillingLineItem.cpt field, diff --git a/canvas_sdk/v1/data/condition.py b/canvas_sdk/v1/data/condition.py index 7608a0a8..691aaaa2 100644 --- a/canvas_sdk/v1/data/condition.py +++ b/canvas_sdk/v1/data/condition.py @@ -4,13 +4,12 @@ from django.db.models import TextChoices from canvas_sdk.v1.data.base import ( + BaseModelManager, BaseQuerySet, CommittableQuerySetMixin, - PatientAssetQuerySetMixin, + ForPatientQuerySetMixin, ValueSetLookupQuerySetMixin, ) -from canvas_sdk.v1.data.patient import Patient -from canvas_sdk.v1.data.user import CanvasUser class ClinicalStatus(TextChoices): @@ -25,15 +24,18 @@ class ClinicalStatus(TextChoices): class ConditionQuerySet( BaseQuerySet, - ValueSetLookupQuerySetMixin["ConditionQuerySet"], - CommittableQuerySetMixin["ConditionQuerySet"], - PatientAssetQuerySetMixin["ConditionQuerySet"], + ValueSetLookupQuerySetMixin, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, ): """ConditionQuerySet.""" pass +ConditionManager = BaseModelManager.from_queryset(ConditionQuerySet) + + class Condition(models.Model): """Condition.""" @@ -41,7 +43,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_condition_001" - objects = cast(ConditionQuerySet, ConditionQuerySet.as_manager()) + objects = cast(ConditionQuerySet, ConditionManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/v1/data/lab.py b/canvas_sdk/v1/data/lab.py index a50b51db..b684d7e5 100644 --- a/canvas_sdk/v1/data/lab.py +++ b/canvas_sdk/v1/data/lab.py @@ -1,12 +1,26 @@ +from typing import cast + from django.db import models from canvas_sdk.v1.data.base import ( - CommittableModelManager, + BaseModelManager, + BaseQuerySet, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, TimeframeLookupQuerySetMixin, ValueSetLookupQuerySet, ) +class LabReportQuerySet(BaseQuerySet, CommittableQuerySetMixin, ForPatientQuerySetMixin): + """A queryset for lab reports.""" + + pass + + +LabReportManager = BaseModelManager.from_queryset(LabReportQuerySet) + + class TransmissionType(models.TextChoices): """Choices for transmission types.""" @@ -22,7 +36,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_labreport_001" - objects = CommittableModelManager() + objects = cast(LabReportQuerySet, LabReportManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) @@ -52,6 +66,15 @@ class Meta: deleted = models.BooleanField() +class LabReviewQuerySet(BaseQuerySet, CommittableQuerySetMixin, ForPatientQuerySetMixin): + """A queryset for lab reviews.""" + + pass + + +LabReviewManager = BaseModelManager.from_queryset(LabReviewQuerySet) + + class LabReview(models.Model): """A class representing a lab review.""" @@ -59,7 +82,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_labreview_001" - objects = CommittableModelManager() + objects = cast(LabReviewQuerySet, LabReviewManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/v1/data/medication.py b/canvas_sdk/v1/data/medication.py index d8238d9d..35fc1356 100644 --- a/canvas_sdk/v1/data/medication.py +++ b/canvas_sdk/v1/data/medication.py @@ -1,7 +1,14 @@ +from typing import cast + 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.base import ( + BaseModelManager, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, + ValueSetLookupQuerySet, +) class Status(TextChoices): @@ -11,12 +18,15 @@ class Status(TextChoices): INACTIVE = "inactive", "inactive" -class MedicationQuerySet(ValueSetLookupQuerySet): +class MedicationQuerySet(ValueSetLookupQuerySet, CommittableQuerySetMixin, ForPatientQuerySetMixin): """MedicationQuerySet.""" pass +MedicationManager = BaseModelManager.from_queryset(MedicationQuerySet) + + class Medication(models.Model): """Medication.""" @@ -24,7 +34,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_medication_001" - objects = CommittableModelManager.from_queryset(MedicationQuerySet)() + objects = cast(MedicationQuerySet, MedicationManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/v1/data/observation.py b/canvas_sdk/v1/data/observation.py index a3c41b69..15f4d017 100644 --- a/canvas_sdk/v1/data/observation.py +++ b/canvas_sdk/v1/data/observation.py @@ -1,14 +1,26 @@ +from typing import cast + from django.db import models -from canvas_sdk.v1.data.base import CommittableModelManager, ValueSetLookupQuerySet +from canvas_sdk.v1.data.base import ( + BaseModelManager, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, + ValueSetLookupQuerySet, +) -class ObservationQuerySet(ValueSetLookupQuerySet): +class ObservationQuerySet( + ValueSetLookupQuerySet, CommittableQuerySetMixin, ForPatientQuerySetMixin +): """ObservationQuerySet.""" pass +ObservationManager = BaseModelManager.from_queryset(ObservationQuerySet) + + class Observation(models.Model): """Observation.""" @@ -16,7 +28,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_observation_001" - objects = CommittableModelManager.from_queryset(ObservationQuerySet)() + objects = cast(ObservationQuerySet, ObservationManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/v1/data/protocol_override.py b/canvas_sdk/v1/data/protocol_override.py index 2b5298bb..836556fd 100644 --- a/canvas_sdk/v1/data/protocol_override.py +++ b/canvas_sdk/v1/data/protocol_override.py @@ -1,6 +1,13 @@ +from typing import cast + from django.db import models -from canvas_sdk.v1.data.base import CommittableModelManager +from canvas_sdk.v1.data.base import ( + BaseModelManager, + BaseQuerySet, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, +) class IntervalUnit(models.TextChoices): @@ -18,6 +25,15 @@ class Status(models.TextChoices): INACTIVE = "inactive", "inactive" +class ProtocolOverrideQuerySet(BaseQuerySet, ForPatientQuerySetMixin, CommittableQuerySetMixin): + """ProtocolOverrideQuerySet.""" + + pass + + +ProtocolOverrideManager = BaseModelManager.from_queryset(ProtocolOverrideQuerySet) + + class ProtocolOverride(models.Model): """ProtocolOverride.""" @@ -25,7 +41,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_protocoloverride_001" - objects = CommittableModelManager() + objects = cast(ProtocolOverrideQuerySet, ProtocolOverrideManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) diff --git a/canvas_sdk/v1/data/questionnaire.py b/canvas_sdk/v1/data/questionnaire.py index fb37e18d..57768a1b 100644 --- a/canvas_sdk/v1/data/questionnaire.py +++ b/canvas_sdk/v1/data/questionnaire.py @@ -1,10 +1,14 @@ from collections.abc import Container +from typing import cast from django.db import models from django.db.models import Q from canvas_sdk.v1.data.base import ( - CommittableModelManager, + BaseModelManager, + BaseQuerySet, + CommittableQuerySetMixin, + ForPatientQuerySetMixin, ValueSetLookupByNameQuerySet, ) @@ -123,6 +127,15 @@ class Meta: question = models.ForeignKey(Question, on_delete=models.DO_NOTHING, null=True) +class InterviewQuerySet(BaseQuerySet, ForPatientQuerySetMixin, CommittableQuerySetMixin): + """InterviewQuerySet.""" + + pass + + +InterviewManager = BaseModelManager.from_queryset(InterviewQuerySet) + + class Interview(models.Model): """Interview.""" @@ -130,7 +143,7 @@ class Meta: managed = False db_table = "canvas_sdk_data_api_interview_001" - objects = CommittableModelManager() + objects = cast(InterviewQuerySet, InterviewManager()) id = models.UUIDField() dbid = models.BigIntegerField(primary_key=True) From 793a6ff3d6c5739f2679516020fce28527c12444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Magalh=C3=A3es?= Date: Tue, 17 Dec 2024 14:51:53 +0000 Subject: [PATCH 3/4] improvements based on feedback --- 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 15d38c2f..c2e9fc21 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -177,7 +177,7 @@ class ValueSetLookupQuerySet(BaseQuerySet, ValueSetLookupQuerySetMixin): pass -class ValueSetLookupByNameQuerySet(CommittableQuerySet, ValueSetLookupByNameQuerySetMixin): +class ValueSetLookupByNameQuerySet(BaseQuerySet, ValueSetLookupByNameQuerySetMixin): """A class that includes methods for looking up value sets by name.""" pass From 6555fc20e22199271b8fdde7cccd06c20f84e9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Magalh=C3=A3es?= Date: Mon, 27 Jan 2025 09:17:20 +0000 Subject: [PATCH 4/4] fix: BaseManager get_queryset --- 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 c2e9fc21..e3236bdb 100644 --- a/canvas_sdk/v1/data/base.py +++ b/canvas_sdk/v1/data/base.py @@ -15,7 +15,7 @@ class BaseModelManager(models.Manager): def get_queryset(self) -> models.QuerySet: """Return a queryset that filters out deleted objects.""" - return self.filter(deleted=False) + return super().get_queryset().filter(deleted=False) class BaseQuerySet(models.QuerySet):