canvas 0.15.0__py3-none-any.whl → 0.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of canvas might be problematic. Click here for more details.

Files changed (46) hide show
  1. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/METADATA +2 -2
  2. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/RECORD +46 -32
  3. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +6 -3
  4. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py +4 -1
  5. canvas_cli/utils/validators/manifest_schema.py +9 -2
  6. canvas_generated/messages/effects_pb2.py +2 -2
  7. canvas_generated/messages/effects_pb2.pyi +14 -0
  8. canvas_generated/messages/events_pb2.py +2 -2
  9. canvas_generated/messages/events_pb2.pyi +40 -0
  10. canvas_sdk/effects/launch_modal.py +14 -3
  11. canvas_sdk/handlers/action_button.py +33 -16
  12. canvas_sdk/templates/__init__.py +3 -0
  13. canvas_sdk/templates/tests/__init__.py +0 -0
  14. canvas_sdk/templates/tests/test_utils.py +43 -0
  15. canvas_sdk/templates/utils.py +44 -0
  16. canvas_sdk/v1/data/__init__.py +23 -1
  17. canvas_sdk/v1/data/allergy_intolerance.py +22 -2
  18. canvas_sdk/v1/data/appointment.py +56 -0
  19. canvas_sdk/v1/data/assessment.py +40 -0
  20. canvas_sdk/v1/data/base.py +35 -22
  21. canvas_sdk/v1/data/billing.py +2 -2
  22. canvas_sdk/v1/data/care_team.py +60 -0
  23. canvas_sdk/v1/data/command.py +1 -1
  24. canvas_sdk/v1/data/common.py +53 -0
  25. canvas_sdk/v1/data/condition.py +19 -3
  26. canvas_sdk/v1/data/coverage.py +294 -0
  27. canvas_sdk/v1/data/detected_issue.py +1 -0
  28. canvas_sdk/v1/data/lab.py +26 -3
  29. canvas_sdk/v1/data/medication.py +13 -3
  30. canvas_sdk/v1/data/note.py +5 -1
  31. canvas_sdk/v1/data/observation.py +15 -3
  32. canvas_sdk/v1/data/patient.py +140 -1
  33. canvas_sdk/v1/data/protocol_override.py +18 -2
  34. canvas_sdk/v1/data/questionnaire.py +15 -2
  35. canvas_sdk/value_set/hcc2018.py +55369 -0
  36. plugin_runner/sandbox.py +28 -0
  37. plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +47 -0
  38. plugin_runner/tests/fixtures/plugins/test_render_template/README.md +11 -0
  39. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
  40. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +43 -0
  41. plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +10 -0
  42. plugin_runner/tests/test_plugin_runner.py +0 -46
  43. plugin_runner/tests/test_sandbox.py +21 -1
  44. settings.py +10 -0
  45. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/WHEEL +0 -0
  46. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,7 @@
1
1
  from enum import StrEnum
2
- from typing import Any
2
+ from typing import Any, Self
3
+
4
+ from pydantic import model_validator
3
5
 
4
6
  from canvas_sdk.effects import EffectType, _BaseEffect
5
7
 
@@ -15,10 +17,19 @@ class LaunchModalEffect(_BaseEffect):
15
17
  NEW_WINDOW = "new_window"
16
18
  RIGHT_CHART_PANE = "right_chart_pane"
17
19
 
18
- url: str
20
+ url: str | None = None
21
+ content: str | None = None
19
22
  target: TargetType = TargetType.DEFAULT_MODAL
20
23
 
21
24
  @property
22
25
  def values(self) -> dict[str, Any]:
23
26
  """The LaunchModalEffect values."""
24
- return {"url": self.url, "target": self.target.value}
27
+ return {"url": self.url, "content": self.content, "target": self.target.value}
28
+
29
+ @model_validator(mode="after")
30
+ def check_mutually_exclusive_fields(self) -> Self:
31
+ """Check that url and content are mutually exclusive."""
32
+ if self.url is not None and self.content is not None:
33
+ raise ValueError("'url' and 'content' are mutually exclusive")
34
+
35
+ return self
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from abc import abstractmethod
2
3
  from enum import StrEnum
3
4
 
@@ -6,6 +7,8 @@ from canvas_sdk.effects.show_button import ShowButtonEffect
6
7
  from canvas_sdk.events import EventType
7
8
  from canvas_sdk.handlers.base import BaseHandler
8
9
 
10
+ SHOW_BUTTON_REGEX = re.compile(r"^SHOW_(.+?)_BUTTON$")
11
+
9
12
 
10
13
  class ActionButton(BaseHandler):
11
14
  """Base class for action buttons."""
@@ -13,16 +16,38 @@ class ActionButton(BaseHandler):
13
16
  RESPONDS_TO = [
14
17
  EventType.Name(EventType.SHOW_NOTE_HEADER_BUTTON),
15
18
  EventType.Name(EventType.SHOW_NOTE_FOOTER_BUTTON),
19
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_SOCIAL_DETERMINANTS_SECTION_BUTTON),
20
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_GOALS_SECTION_BUTTON),
21
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_CONDITIONS_SECTION_BUTTON),
22
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_MEDICATIONS_SECTION_BUTTON),
23
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_ALLERGIES_SECTION_BUTTON),
24
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_CARE_TEAMS_SECTION_BUTTON),
25
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_VITALS_SECTION_BUTTON),
26
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_IMMUNIZATIONS_SECTION_BUTTON),
27
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_SURGICAL_HISTORY_SECTION_BUTTON),
28
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_FAMILY_HISTORY_SECTION_BUTTON),
29
+ EventType.Name(EventType.SHOW_CHART_SUMMARY_CODING_GAPS_SECTION_BUTTON),
16
30
  EventType.Name(EventType.ACTION_BUTTON_CLICKED),
17
31
  ]
18
32
 
19
33
  class ButtonLocation(StrEnum):
20
34
  NOTE_HEADER = "note_header"
21
35
  NOTE_FOOTER = "note_footer"
36
+ CHART_SUMMARY_SOCIAL_DETERMINANTS_SECTION = "chart_summary_social_determinants_section"
37
+ CHART_SUMMARY_GOALS_SECTION = "chart_summary_goals_section"
38
+ CHART_SUMMARY_CONDITIONS_SECTION = "chart_summary_conditions_section"
39
+ CHART_SUMMARY_MEDICATIONS_SECTION = "chart_summary_medications_section"
40
+ CHART_SUMMARY_ALLERGIES_SECTION = "chart_summary_allergies_section"
41
+ CHART_SUMMARY_CARE_TEAMS_SECTION = "chart_summary_care_teams_section"
42
+ CHART_SUMMARY_VITALS_SECTION = "chart_summary_vitals_section"
43
+ CHART_SUMMARY_IMMUNIZATIONS_SECTION = "chart_summary_immunizations_section"
44
+ CHART_SUMMARY_SURGICAL_HISTORY_SECTION = "chart_summary_surgical_history_section"
45
+ CHART_SUMMARY_FAMILY_HISTORY_SECTION = "chart_summary_family_history_section"
46
+ CHART_SUMMARY_CODING_GAPS_SECTION = "chart_summary_coding_gaps_section"
22
47
 
23
48
  BUTTON_TITLE: str = ""
24
49
  BUTTON_KEY: str = ""
25
- BUTTON_LOCATION: ButtonLocation | None = None
50
+ BUTTON_LOCATION: ButtonLocation
26
51
 
27
52
  @abstractmethod
28
53
  def handle(self) -> list[Effect]:
@@ -35,24 +60,16 @@ class ActionButton(BaseHandler):
35
60
 
36
61
  def compute(self) -> list[Effect]:
37
62
  """Method to compute the effects."""
38
- if self.BUTTON_LOCATION is None:
63
+ if not self.BUTTON_LOCATION:
39
64
  return []
40
65
 
41
- if self.event.type in (
42
- EventType.SHOW_NOTE_HEADER_BUTTON,
43
- EventType.SHOW_NOTE_FOOTER_BUTTON,
44
- ):
45
- if (
46
- self.event.context["location"].lower() == self.BUTTON_LOCATION.value
47
- and self.visible()
48
- ):
66
+ show_button_event_match = SHOW_BUTTON_REGEX.fullmatch(self.event.name)
67
+
68
+ if show_button_event_match:
69
+ location = show_button_event_match.group(1)
70
+ if self.ButtonLocation[location] == self.BUTTON_LOCATION and self.visible():
49
71
  return [ShowButtonEffect(key=self.BUTTON_KEY, title=self.BUTTON_TITLE).apply()]
50
- else:
51
- return []
52
- elif (
53
- self.event.type == EventType.ACTION_BUTTON_CLICKED
54
- and self.event.context["key"] == self.BUTTON_KEY
55
- ):
72
+ elif self.context["key"] == self.BUTTON_KEY:
56
73
  return self.handle()
57
74
 
58
75
  return []
@@ -0,0 +1,3 @@
1
+ from .utils import render_to_string
2
+
3
+ __all__ = ("render_to_string",)
File without changes
@@ -0,0 +1,43 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from canvas_sdk.effects import Effect
6
+ from canvas_sdk.events import Event, EventRequest, EventType
7
+ from plugin_runner.plugin_runner import LOADED_PLUGINS
8
+
9
+
10
+ @pytest.mark.parametrize("install_test_plugin", ["test_render_template"], indirect=True)
11
+ def test_render_to_string_valid_template(
12
+ install_test_plugin: Path, load_test_plugins: None
13
+ ) -> None:
14
+ """Test that the render_to_string function loads and renders a valid template."""
15
+ plugin = LOADED_PLUGINS[
16
+ "test_render_template:test_render_template.protocols.my_protocol:ValidTemplate"
17
+ ]
18
+ result: list[Effect] = plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
19
+ assert "html" in result[0].payload
20
+
21
+
22
+ @pytest.mark.parametrize("install_test_plugin", ["test_render_template"], indirect=True)
23
+ def test_render_to_string_invalid_template(
24
+ install_test_plugin: Path, load_test_plugins: None
25
+ ) -> None:
26
+ """Test that the render_to_string function raises an error for invalid templates."""
27
+ plugin = LOADED_PLUGINS[
28
+ "test_render_template:test_render_template.protocols.my_protocol:InvalidTemplate"
29
+ ]
30
+ with pytest.raises(FileNotFoundError):
31
+ plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
32
+
33
+
34
+ @pytest.mark.parametrize("install_test_plugin", ["test_render_template"], indirect=True)
35
+ def test_render_to_string_forbidden_template(
36
+ install_test_plugin: Path, load_test_plugins: None
37
+ ) -> None:
38
+ """Test that the render_to_string function raises an error for a template outside plugin package."""
39
+ plugin = LOADED_PLUGINS[
40
+ "test_render_template:test_render_template.protocols.my_protocol:ForbiddenTemplate"
41
+ ]
42
+ with pytest.raises(PermissionError):
43
+ plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
@@ -0,0 +1,44 @@
1
+ import inspect
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from django.template import Context, Template
6
+
7
+ from settings import PLUGIN_DIRECTORY
8
+
9
+
10
+ def render_to_string(template_name: str, context: dict[str, Any] | None = None) -> str | None:
11
+ """Load a template and render it with the given context.
12
+
13
+ Args:
14
+ template_name (str): The path to the template file, relative to the plugin package.
15
+ If the path starts with a forward slash ("/"), it will be stripped during resolution.
16
+ context (dict[str, Any] | None): A dictionary of variables to pass to the template
17
+ for rendering. Defaults to None, which uses an empty context.
18
+
19
+ Returns:
20
+ str: The rendered template as a string.
21
+
22
+ Raises:
23
+ FileNotFoundError: If the template file does not exist within the plugin's directory
24
+ or if the resolved path is invalid.
25
+ """
26
+ plugins_dir = Path(PLUGIN_DIRECTORY).resolve()
27
+ current_frame = inspect.currentframe()
28
+ caller = current_frame.f_back if current_frame else None
29
+
30
+ if not caller or "__is_plugin__" not in caller.f_globals:
31
+ return None
32
+
33
+ plugin_name = caller.f_globals["__name__"].split(".")[0]
34
+ plugin_dir = plugins_dir / plugin_name
35
+ template_path = Path(plugin_dir / template_name.lstrip("/")).resolve()
36
+
37
+ if not template_path.is_relative_to(plugin_dir):
38
+ raise PermissionError(f"Invalid template '{template_name}'")
39
+ elif not template_path.exists():
40
+ raise FileNotFoundError(f"Template {template_name} not found.")
41
+
42
+ template = Template(template_path.read_text())
43
+
44
+ return template.render(Context(context))
@@ -1,7 +1,11 @@
1
1
  from .allergy_intolerance import AllergyIntolerance, AllergyIntoleranceCoding
2
+ from .appointment import Appointment
3
+ from .assessment import Assessment
2
4
  from .billing import BillingLineItem
5
+ from .care_team import CareTeamMembership, CareTeamRole
3
6
  from .command import Command
4
7
  from .condition import Condition, ConditionCoding
8
+ from .coverage import Coverage, Transactor, TransactorAddress, TransactorPhone
5
9
  from .detected_issue import DetectedIssue, DetectedIssueEvidence
6
10
  from .device import Device
7
11
  from .imaging import ImagingOrder, ImagingReport, ImagingReview
@@ -25,7 +29,13 @@ from .observation import (
25
29
  ObservationValueCoding,
26
30
  )
27
31
  from .organization import Organization
28
- from .patient import Patient
32
+ from .patient import (
33
+ Patient,
34
+ PatientAddress,
35
+ PatientContactPoint,
36
+ PatientExternalIdentifier,
37
+ PatientSetting,
38
+ )
29
39
  from .practicelocation import PracticeLocation, PracticeLocationSetting
30
40
  from .protocol_override import ProtocolOverride
31
41
  from .questionnaire import (
@@ -43,13 +53,18 @@ from .task import Task, TaskComment, TaskLabel, TaskTaskLabel
43
53
  from .user import CanvasUser
44
54
 
45
55
  __all__ = [
56
+ "Appointment",
46
57
  "AllergyIntolerance",
47
58
  "AllergyIntoleranceCoding",
59
+ "Assessment",
48
60
  "BillingLineItem",
49
61
  "CanvasUser",
62
+ "CareTeamMembership",
63
+ "CareTeamRole",
50
64
  "Command",
51
65
  "Condition",
52
66
  "ConditionCoding",
67
+ "Coverage",
53
68
  "DetectedIssue",
54
69
  "DetectedIssueEvidence",
55
70
  "Device",
@@ -78,6 +93,10 @@ __all__ = [
78
93
  "ObservationValueCoding",
79
94
  "Organization",
80
95
  "Patient",
96
+ "PatientAddress",
97
+ "PatientContactPoint",
98
+ "PatientExternalIdentifier",
99
+ "PatientSetting",
81
100
  "PracticeLocation",
82
101
  "PracticeLocationSetting",
83
102
  "ProtocolOverride",
@@ -91,4 +110,7 @@ __all__ = [
91
110
  "TaskComment",
92
111
  "TaskLabel",
93
112
  "TaskTaskLabel",
113
+ "Transactor",
114
+ "TransactorAddress",
115
+ "TransactorPhone",
94
116
  ]
@@ -1,6 +1,26 @@
1
+ from typing import cast
2
+
1
3
  from django.db import models
2
4
 
3
- from canvas_sdk.v1.data.base import CommittableModelManager, ValueSetLookupQuerySet
5
+ from canvas_sdk.v1.data.base import (
6
+ BaseModelManager,
7
+ CommittableQuerySetMixin,
8
+ ForPatientQuerySetMixin,
9
+ ValueSetLookupQuerySet,
10
+ )
11
+
12
+
13
+ class AllergyIntoleranceQuerySet(
14
+ ValueSetLookupQuerySet,
15
+ CommittableQuerySetMixin,
16
+ ForPatientQuerySetMixin,
17
+ ):
18
+ """AllergyIntoleranceQuerySet."""
19
+
20
+ pass
21
+
22
+
23
+ AllergyIntoleranceManager = BaseModelManager.from_queryset(AllergyIntoleranceQuerySet)
4
24
 
5
25
 
6
26
  class AllergyIntolerance(models.Model):
@@ -10,7 +30,7 @@ class AllergyIntolerance(models.Model):
10
30
  managed = False
11
31
  db_table = "canvas_sdk_data_api_allergyintolerance_001"
12
32
 
13
- objects = CommittableModelManager().from_queryset(ValueSetLookupQuerySet)()
33
+ objects = cast(AllergyIntoleranceQuerySet, AllergyIntoleranceManager())
14
34
 
15
35
  id = models.UUIDField()
16
36
  dbid = models.BigIntegerField(primary_key=True)
@@ -0,0 +1,56 @@
1
+ from django.db import models
2
+
3
+
4
+ class AppointmentProgressStatus(models.TextChoices):
5
+ """AppointmentProgressStatus."""
6
+
7
+ UNCONFIRMED = "unconfirmed", "Unconfirmed"
8
+ ATTEMPTED = "attempted", "Attempted"
9
+ CONFIRMED = "confirmed", "Confirmed"
10
+ ARRIVED = "arrived", "Arrived"
11
+ ROOMED = "roomed", "Roomed"
12
+ EXITED = "exited", "Exited"
13
+ NOSHOWED = "noshowed", "No-showed"
14
+ CANCELLED = "cancelled", "Cancelled"
15
+
16
+
17
+ class Appointment(models.Model):
18
+ """Appointment."""
19
+
20
+ class Meta:
21
+ managed = False
22
+ db_table = "canvas_sdk_data_api_appointment_001"
23
+
24
+ id = models.UUIDField()
25
+ dbid = models.BigIntegerField(primary_key=True)
26
+ entered_in_error = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING, null=True)
27
+ patient = models.ForeignKey(
28
+ "v1.Patient",
29
+ on_delete=models.DO_NOTHING,
30
+ related_name="appointments",
31
+ null=True,
32
+ )
33
+ appointment_rescheduled_from = models.ForeignKey(
34
+ "self",
35
+ on_delete=models.DO_NOTHING,
36
+ related_name="appointment_rescheduled_to",
37
+ null=True,
38
+ )
39
+ provider = models.ForeignKey("v1.Staff", on_delete=models.DO_NOTHING, null=True)
40
+ start_time = models.DateTimeField()
41
+ duration_minutes = models.IntegerField()
42
+ comment = models.TextField(null=True)
43
+ note = models.ForeignKey("v1.Note", on_delete=models.DO_NOTHING, null=True)
44
+
45
+ note_type = models.ForeignKey(
46
+ "v1.NoteType", on_delete=models.DO_NOTHING, related_name="appointments", null=True
47
+ )
48
+
49
+ status = models.CharField(
50
+ max_length=20,
51
+ choices=AppointmentProgressStatus,
52
+ )
53
+ meeting_link = models.URLField(null=True, blank=True)
54
+ telehealth_instructions_sent = models.BooleanField()
55
+ location = models.ForeignKey("v1.PracticeLocation", on_delete=models.DO_NOTHING, null=True)
56
+ description = models.TextField(null=True, blank=True)
@@ -0,0 +1,40 @@
1
+ from django.db import models
2
+
3
+
4
+ class AssessmentStatus(models.TextChoices):
5
+ """AssessmentStatus."""
6
+
7
+ STATUS_IMPROVING = "improved", "Improved"
8
+ STATUS_STABLE = "stable", "Unchanged"
9
+ STATUS_DETERIORATING = "deteriorated", "Deteriorated"
10
+
11
+
12
+ class Assessment(models.Model):
13
+ """Assessment."""
14
+
15
+ class Meta:
16
+ managed = False
17
+ db_table = "canvas_sdk_data_api_assessment_001"
18
+
19
+ id = models.UUIDField()
20
+ dbid = models.BigIntegerField(primary_key=True)
21
+ created = models.DateTimeField()
22
+ modified = models.DateTimeField()
23
+ originator = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING)
24
+ committer = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING, null=True)
25
+ deleted = models.BooleanField()
26
+ entered_in_error = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING, null=True)
27
+ patient = models.ForeignKey(
28
+ "v1.Patient",
29
+ on_delete=models.DO_NOTHING,
30
+ related_name="assessments",
31
+ )
32
+ note = models.ForeignKey("v1.Note", on_delete=models.DO_NOTHING, related_name="assessments")
33
+ condition = models.ForeignKey(
34
+ "v1.Condition", on_delete=models.CASCADE, related_name="assessments", null=True
35
+ )
36
+ interview = models.ForeignKey("v1.Interview", on_delete=models.DO_NOTHING, null=True)
37
+ status = models.CharField(choices=AssessmentStatus.choices)
38
+ narrative = models.CharField()
39
+ background = models.CharField()
40
+ care_team = models.CharField()
@@ -10,25 +10,12 @@ if TYPE_CHECKING:
10
10
  from canvas_sdk.value_set.value_set import ValueSet
11
11
 
12
12
 
13
- class CommittableModelManager(models.Manager):
14
- """A manager for commands that can be committed."""
13
+ class BaseModelManager(models.Manager):
14
+ """A base manager for models."""
15
15
 
16
- def get_queryset(self) -> "CommittableQuerySet":
16
+ def get_queryset(self) -> models.QuerySet:
17
17
  """Return a queryset that filters out deleted objects."""
18
- # TODO: Should we just filter these out at the view level?
19
- return CommittableQuerySet(self.model, using=self._db).filter(deleted=False)
20
-
21
-
22
- class CommittableQuerySet(models.QuerySet):
23
- """A queryset for committable objects."""
24
-
25
- def committed(self) -> "Self":
26
- """Return a queryset that filters for objects that have been committed."""
27
- return self.filter(committer_id__isnull=False, entered_in_error_id__isnull=True)
28
-
29
- def for_patient(self, patient_id: str) -> "Self":
30
- """Return a queryset that filters objects for a specific patient."""
31
- return self.filter(patient__id=patient_id)
18
+ return super().get_queryset().filter(deleted=False)
32
19
 
33
20
 
34
21
  class BaseQuerySet(models.QuerySet):
@@ -40,10 +27,14 @@ class BaseQuerySet(models.QuerySet):
40
27
  class QuerySetProtocol(Protocol):
41
28
  """A typing protocol for use in mixins into models.QuerySet-inherited classes."""
42
29
 
43
- def filter(self, *args: Any, **kwargs: Any) -> models.QuerySet[Any]:
30
+ def filter(self, *args: Any, **kwargs: Any) -> Self:
44
31
  """Django's models.QuerySet filter method."""
45
32
  ...
46
33
 
34
+ def distinct(self) -> Self:
35
+ """Django's models.QuerySet distinct method."""
36
+ ...
37
+
47
38
 
48
39
  class ValueSetLookupQuerySetProtocol(QuerySetProtocol):
49
40
  """A typing protocol for use in mixins using value set lookup methods."""
@@ -61,10 +52,26 @@ class ValueSetLookupQuerySetProtocol(QuerySetProtocol):
61
52
  raise NotImplementedError
62
53
 
63
54
 
55
+ class CommittableQuerySetMixin(QuerySetProtocol):
56
+ """A queryset for committable objects."""
57
+
58
+ def committed(self) -> Self:
59
+ """Return a queryset that filters for objects that have been committed."""
60
+ return self.filter(committer_id__isnull=False, entered_in_error_id__isnull=True)
61
+
62
+
63
+ class ForPatientQuerySetMixin(QuerySetProtocol):
64
+ """A queryset for patient assets."""
65
+
66
+ def for_patient(self, patient_id: str) -> Self:
67
+ """Return a queryset that filters objects for a specific patient."""
68
+ return self.filter(patient__id=patient_id)
69
+
70
+
64
71
  class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol):
65
72
  """A QuerySet mixin that can filter objects based on a ValueSet."""
66
73
 
67
- def find(self, value_set: type["ValueSet"]) -> models.QuerySet[Any]:
74
+ def find(self, value_set: type["ValueSet"]) -> Self:
68
75
  """
69
76
  Filters conditions, medications, etc. to those found in the inherited ValueSet class that is passed.
70
77
 
@@ -146,7 +153,7 @@ class TimeframeLookupQuerySetMixin(TimeframeLookupQuerySetProtocol):
146
153
  """Returns the field that should be filtered on. Can be overridden for different models."""
147
154
  return "note__datetime_of_service"
148
155
 
149
- def within(self, timeframe: "Timeframe") -> models.QuerySet:
156
+ def within(self, timeframe: "Timeframe") -> Self:
150
157
  """A method to filter a queryset for datetimes within a timeframe."""
151
158
  return self.filter(
152
159
  **{
@@ -158,13 +165,19 @@ class TimeframeLookupQuerySetMixin(TimeframeLookupQuerySetProtocol):
158
165
  )
159
166
 
160
167
 
161
- class ValueSetLookupQuerySet(CommittableQuerySet, ValueSetLookupQuerySetMixin):
168
+ class CommittableQuerySet(BaseQuerySet, CommittableQuerySetMixin):
169
+ """A queryset for committable objects."""
170
+
171
+ pass
172
+
173
+
174
+ class ValueSetLookupQuerySet(BaseQuerySet, ValueSetLookupQuerySetMixin):
162
175
  """A class that includes methods for looking up value sets."""
163
176
 
164
177
  pass
165
178
 
166
179
 
167
- class ValueSetLookupByNameQuerySet(CommittableQuerySet, ValueSetLookupByNameQuerySetMixin):
180
+ class ValueSetLookupByNameQuerySet(BaseQuerySet, ValueSetLookupByNameQuerySetMixin):
168
181
  """A class that includes methods for looking up value sets by name."""
169
182
 
170
183
  pass
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, Self
2
2
 
3
3
  from django.db import models
4
4
 
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
  class BillingLineItemQuerySet(ValueSetTimeframeLookupQuerySet):
13
13
  """A class that adds functionality to filter BillingLineItem objects."""
14
14
 
15
- def find(self, value_set: type["ValueSet"]) -> models.QuerySet:
15
+ def find(self, value_set: type["ValueSet"]) -> Self:
16
16
  """
17
17
  This method is overridden to use for BillingLineItem CPT codes.
18
18
  The codes are saved as string values in the BillingLineItem.cpt field,
@@ -0,0 +1,60 @@
1
+ from django.db import models
2
+
3
+
4
+ class CareTeamMembershipStatus(models.TextChoices):
5
+ """CareTeamMembershipStatus."""
6
+
7
+ PROPOSED = "proposed", "Proposed"
8
+ ACTIVE = "active", "Active"
9
+ SUSPENDED = "suspended", "Suspended"
10
+ INACTIVE = "inactive", "Inactive"
11
+ ENTERED_IN_ERROR = "entered-in-error", "Entered in Error"
12
+
13
+
14
+ class CareTeamRole(models.Model):
15
+ """CareTeamRole."""
16
+
17
+ class Meta:
18
+ managed = False
19
+ db_table = "canvas_sdk_data_api_careteamrole_001"
20
+
21
+ dbid = models.BigIntegerField(primary_key=True)
22
+ system = models.CharField()
23
+ version = models.CharField()
24
+ code = models.CharField()
25
+ display = models.CharField()
26
+ user_selected = models.BooleanField()
27
+ active = models.BooleanField()
28
+
29
+ def __str__(self) -> str:
30
+ return self.display
31
+
32
+
33
+ class CareTeamMembership(models.Model):
34
+ """CareTeamMembership."""
35
+
36
+ class Meta:
37
+ managed = False
38
+ db_table = "canvas_sdk_data_api_careteammembership_001"
39
+
40
+ id = models.UUIDField()
41
+ dbid = models.BigIntegerField(primary_key=True)
42
+ created = models.DateTimeField()
43
+ modified = models.DateTimeField()
44
+ patient = models.ForeignKey(
45
+ "v1.Patient", on_delete=models.DO_NOTHING, related_name="care_team_memberships", null=True
46
+ )
47
+ staff = models.ForeignKey(
48
+ "v1.Staff", on_delete=models.DO_NOTHING, related_name="care_team_memberships", null=True
49
+ )
50
+ role = models.ForeignKey(
51
+ "v1.CareTeamRole", related_name="care_teams", on_delete=models.DO_NOTHING, null=True
52
+ )
53
+ status = models.CharField(choices=CareTeamMembershipStatus.choices)
54
+ lead = models.BooleanField()
55
+ role_code = models.CharField()
56
+ role_system = models.CharField()
57
+ role_display = models.CharField()
58
+
59
+ def __str__(self) -> str:
60
+ return f"id={self.id}"
@@ -18,7 +18,7 @@ class Command(models.Model):
18
18
  entered_in_error = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING, null=True)
19
19
  state = models.CharField()
20
20
  patient = models.ForeignKey("v1.Patient", on_delete=models.DO_NOTHING, null=True)
21
- note_id = models.BigIntegerField()
21
+ note = models.ForeignKey("v1.Note", on_delete=models.DO_NOTHING, related_name="commands")
22
22
  schema_key = models.TextField()
23
23
  data = models.JSONField()
24
24
  origination_source = models.CharField()
@@ -101,3 +101,56 @@ class Origin(models.TextChoices):
101
101
  FLAGGED_POSTING_REVIEW = ("FLG_PST_REV", "Flagged posting review")
102
102
  BATCH_PATIENT_STATEMENTS = ("BAT_PTN_STA", "Batch patient statements")
103
103
  INCOMPLETE_COVERAGE = ("INC_COV", "Incomplete Coverage")
104
+
105
+
106
+ class ContactPointSystem(models.TextChoices):
107
+ """ContactPointSystem."""
108
+
109
+ PHONE = "phone", "phone"
110
+ FAX = "fax", "fax"
111
+ EMAIL = "email", "email"
112
+ PAGER = "pager", "pager"
113
+ OTHER = "other", "other"
114
+
115
+
116
+ class ContactPointUse(models.TextChoices):
117
+ """ContactPointUse."""
118
+
119
+ HOME = "home", "Home"
120
+ WORK = "work", "Work"
121
+ TEMP = "temp", "Temp"
122
+ OLD = "old", "Old"
123
+ OTHER = "other", "Other"
124
+ MOBILE = "mobile", "Mobile"
125
+ AUTOMATION = "automation", "Automation"
126
+
127
+
128
+ class ContactPointState(models.TextChoices):
129
+ """ContactPointState."""
130
+
131
+ ACTIVE = "active", "Active"
132
+ DELETED = "deleted", "Deleted"
133
+
134
+
135
+ class AddressUse(models.TextChoices):
136
+ """AddressUse."""
137
+
138
+ HOME = "home", "Home"
139
+ WORK = "work", "Work"
140
+ TEMP = "temp", "Temp"
141
+ OLD = "old", "Old"
142
+
143
+
144
+ class AddressType(models.TextChoices):
145
+ """AddressType."""
146
+
147
+ POSTAL = "postal", "Postal"
148
+ PHYSICAL = "physical", "Physical"
149
+ BOTH = "both", "Both"
150
+
151
+
152
+ class AddressState(models.TextChoices):
153
+ """AddressState."""
154
+
155
+ ACTIVE = "active", "Active"
156
+ DELETED = "deleted", "Deleted"