canvas 0.14.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 (88) hide show
  1. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/METADATA +2 -2
  2. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/RECORD +87 -52
  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/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +1 -1
  6. canvas_cli/utils/validators/manifest_schema.py +9 -2
  7. canvas_generated/messages/effects_pb2.py +2 -2
  8. canvas_generated/messages/effects_pb2.pyi +18 -0
  9. canvas_generated/messages/events_pb2.py +2 -2
  10. canvas_generated/messages/events_pb2.pyi +48 -0
  11. canvas_sdk/commands/tests/protocol/tests.py +3 -1
  12. canvas_sdk/commands/tests/test_utils.py +76 -18
  13. canvas_sdk/effects/banner_alert/tests.py +41 -20
  14. canvas_sdk/effects/launch_modal.py +14 -3
  15. canvas_sdk/events/base.py +1 -3
  16. canvas_sdk/handlers/action_button.py +33 -13
  17. canvas_sdk/handlers/application.py +1 -1
  18. canvas_sdk/handlers/cron_task.py +1 -1
  19. canvas_sdk/protocols/clinical_quality_measure.py +1 -1
  20. canvas_sdk/templates/__init__.py +3 -0
  21. canvas_sdk/templates/tests/__init__.py +0 -0
  22. canvas_sdk/templates/tests/test_utils.py +43 -0
  23. canvas_sdk/templates/utils.py +44 -0
  24. canvas_sdk/v1/apps.py +7 -0
  25. canvas_sdk/v1/data/__init__.py +98 -5
  26. canvas_sdk/v1/data/allergy_intolerance.py +25 -9
  27. canvas_sdk/v1/data/appointment.py +56 -0
  28. canvas_sdk/v1/data/assessment.py +40 -0
  29. canvas_sdk/v1/data/base.py +35 -22
  30. canvas_sdk/v1/data/billing.py +4 -7
  31. canvas_sdk/v1/data/care_team.py +60 -0
  32. canvas_sdk/v1/data/command.py +8 -10
  33. canvas_sdk/v1/data/common.py +53 -0
  34. canvas_sdk/v1/data/condition.py +22 -10
  35. canvas_sdk/v1/data/coverage.py +294 -0
  36. canvas_sdk/v1/data/detected_issue.py +5 -9
  37. canvas_sdk/v1/data/device.py +4 -8
  38. canvas_sdk/v1/data/imaging.py +12 -17
  39. canvas_sdk/v1/data/lab.py +41 -31
  40. canvas_sdk/v1/data/medication.py +16 -10
  41. canvas_sdk/v1/data/note.py +11 -14
  42. canvas_sdk/v1/data/observation.py +19 -14
  43. canvas_sdk/v1/data/organization.py +1 -2
  44. canvas_sdk/v1/data/patient.py +140 -2
  45. canvas_sdk/v1/data/practicelocation.py +2 -4
  46. canvas_sdk/v1/data/protocol_override.py +21 -8
  47. canvas_sdk/v1/data/questionnaire.py +20 -17
  48. canvas_sdk/v1/data/staff.py +5 -7
  49. canvas_sdk/v1/data/task.py +5 -11
  50. canvas_sdk/v1/data/user.py +0 -1
  51. canvas_sdk/v1/models.py +4 -0
  52. canvas_sdk/value_set/hcc2018.py +55369 -0
  53. plugin_runner/plugin_installer.py +1 -1
  54. plugin_runner/plugin_runner.py +5 -25
  55. plugin_runner/sandbox.py +133 -9
  56. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/CANVAS_MANIFEST.json +38 -0
  57. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/README.md +11 -0
  58. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/__init__.py +0 -0
  59. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/my_protocol.py +33 -0
  60. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__init__.py +3 -0
  61. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py +6 -0
  62. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py +5 -0
  63. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py +4 -0
  64. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json +29 -0
  65. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md +12 -0
  66. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py +0 -0
  67. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/base.py +10 -0
  68. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py +0 -0
  69. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py +18 -0
  70. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/CANVAS_MANIFEST.json +29 -0
  71. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/README.md +12 -0
  72. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/__init__.py +0 -0
  73. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/base.py +10 -0
  74. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/__init__.py +0 -0
  75. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/my_protocol.py +18 -0
  76. plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +47 -0
  77. plugin_runner/tests/fixtures/plugins/test_render_template/README.md +11 -0
  78. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
  79. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +43 -0
  80. plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +10 -0
  81. plugin_runner/tests/test_application.py +9 -9
  82. plugin_runner/tests/test_plugin_installer.py +12 -1
  83. plugin_runner/tests/test_plugin_runner.py +159 -66
  84. plugin_runner/tests/test_sandbox.py +26 -14
  85. settings.py +13 -1
  86. canvas_sdk/models/__init__.py +0 -8
  87. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/WHEEL +0 -0
  88. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/entry_points.txt +0 -0
@@ -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,21 +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 self.context["location"].lower() == self.BUTTON_LOCATION.value and self.visible():
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():
46
71
  return [ShowButtonEffect(key=self.BUTTON_KEY, title=self.BUTTON_TITLE).apply()]
47
- else:
48
- return []
49
- elif (
50
- self.event.type == EventType.ACTION_BUTTON_CLICKED
51
- and self.context["key"] == self.BUTTON_KEY
52
- ):
72
+ elif self.context["key"] == self.BUTTON_KEY:
53
73
  return self.handle()
54
74
 
55
75
  return []
@@ -14,7 +14,7 @@ class Application(BaseHandler, ABC):
14
14
  """Handle the application events."""
15
15
  match self.event.type:
16
16
  case EventType.APPLICATION__ON_OPEN:
17
- return [self.on_open()] if self.target == self.identifier else []
17
+ return [self.on_open()] if self.event.target.id == self.identifier else []
18
18
  case _:
19
19
  return []
20
20
 
@@ -29,7 +29,7 @@ class CronTask(BaseHandler):
29
29
  """
30
30
  if not self.SCHEDULE:
31
31
  raise ValueError("You must set a SCHEDULE.")
32
- datetime = arrow.get(self.target).datetime
32
+ datetime = arrow.get(self.event.target.id).datetime
33
33
  if datetime in Cron(self.SCHEDULE):
34
34
  return self.execute()
35
35
  return []
@@ -94,7 +94,7 @@ class ClinicalQualityMeasure(BaseProtocol):
94
94
 
95
95
  def patient_id(model: "type[Model]") -> str:
96
96
  if model == Patient:
97
- return self.target
97
+ return self.event.target.id
98
98
  else:
99
99
  return cast(
100
100
  str,
@@ -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))
canvas_sdk/v1/apps.py ADDED
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class CanvasSdkV1Config(AppConfig):
5
+ """AppConfig for v1 of the SDK data module."""
6
+
7
+ name = "canvas_sdk.v1"
@@ -1,23 +1,116 @@
1
+ from .allergy_intolerance import AllergyIntolerance, AllergyIntoleranceCoding
2
+ from .appointment import Appointment
3
+ from .assessment import Assessment
1
4
  from .billing import BillingLineItem
5
+ from .care_team import CareTeamMembership, CareTeamRole
6
+ from .command import Command
2
7
  from .condition import Condition, ConditionCoding
8
+ from .coverage import Coverage, Transactor, TransactorAddress, TransactorPhone
9
+ from .detected_issue import DetectedIssue, DetectedIssueEvidence
10
+ from .device import Device
11
+ from .imaging import ImagingOrder, ImagingReport, ImagingReview
12
+ from .lab import (
13
+ LabOrder,
14
+ LabOrderReason,
15
+ LabOrderReasonCondition,
16
+ LabReport,
17
+ LabReview,
18
+ LabTest,
19
+ LabValue,
20
+ LabValueCoding,
21
+ )
3
22
  from .medication import Medication, MedicationCoding
23
+ from .note import Note, NoteType
24
+ from .observation import (
25
+ Observation,
26
+ ObservationCoding,
27
+ ObservationComponent,
28
+ ObservationComponentCoding,
29
+ ObservationValueCoding,
30
+ )
4
31
  from .organization import Organization
5
- from .patient import Patient
6
- from .practicelocation import PracticeLocation
32
+ from .patient import (
33
+ Patient,
34
+ PatientAddress,
35
+ PatientContactPoint,
36
+ PatientExternalIdentifier,
37
+ PatientSetting,
38
+ )
39
+ from .practicelocation import PracticeLocation, PracticeLocationSetting
40
+ from .protocol_override import ProtocolOverride
41
+ from .questionnaire import (
42
+ Interview,
43
+ InterviewQuestionnaireMap,
44
+ InterviewQuestionResponse,
45
+ Question,
46
+ Questionnaire,
47
+ QuestionnaireQuestionMap,
48
+ ResponseOption,
49
+ ResponseOptionSet,
50
+ )
7
51
  from .staff import Staff
8
- from .task import Task, TaskComment, TaskLabel
52
+ from .task import Task, TaskComment, TaskLabel, TaskTaskLabel
53
+ from .user import CanvasUser
9
54
 
10
- __all__ = (
55
+ __all__ = [
56
+ "Appointment",
57
+ "AllergyIntolerance",
58
+ "AllergyIntoleranceCoding",
59
+ "Assessment",
11
60
  "BillingLineItem",
61
+ "CanvasUser",
62
+ "CareTeamMembership",
63
+ "CareTeamRole",
64
+ "Command",
12
65
  "Condition",
13
66
  "ConditionCoding",
67
+ "Coverage",
68
+ "DetectedIssue",
69
+ "DetectedIssueEvidence",
70
+ "Device",
71
+ "ImagingOrder",
72
+ "ImagingReport",
73
+ "ImagingReview",
74
+ "Interview",
75
+ "InterviewQuestionnaireMap",
76
+ "InterviewQuestionResponse",
77
+ "LabOrder",
78
+ "LabOrderReason",
79
+ "LabOrderReasonCondition",
80
+ "LabReport",
81
+ "LabReview",
82
+ "LabTest",
83
+ "LabValue",
84
+ "LabValueCoding",
14
85
  "Medication",
15
86
  "MedicationCoding",
87
+ "Note",
88
+ "NoteType",
89
+ "Observation",
90
+ "ObservationCoding",
91
+ "ObservationComponent",
92
+ "ObservationComponentCoding",
93
+ "ObservationValueCoding",
16
94
  "Organization",
17
95
  "Patient",
96
+ "PatientAddress",
97
+ "PatientContactPoint",
98
+ "PatientExternalIdentifier",
99
+ "PatientSetting",
18
100
  "PracticeLocation",
101
+ "PracticeLocationSetting",
102
+ "ProtocolOverride",
103
+ "Question",
104
+ "Questionnaire",
105
+ "QuestionnaireQuestionMap",
106
+ "ResponseOption",
107
+ "ResponseOptionSet",
19
108
  "Staff",
20
109
  "Task",
21
110
  "TaskComment",
22
111
  "TaskLabel",
23
- )
112
+ "TaskTaskLabel",
113
+ "Transactor",
114
+ "TransactorAddress",
115
+ "TransactorPhone",
116
+ ]
@@ -1,8 +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
4
- from canvas_sdk.v1.data.patient import Patient
5
- from canvas_sdk.v1.data.user import CanvasUser
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)
6
24
 
7
25
 
8
26
  class AllergyIntolerance(models.Model):
@@ -10,20 +28,19 @@ class AllergyIntolerance(models.Model):
10
28
 
11
29
  class Meta:
12
30
  managed = False
13
- app_label = "canvas_sdk"
14
31
  db_table = "canvas_sdk_data_api_allergyintolerance_001"
15
32
 
16
- objects = CommittableModelManager().from_queryset(ValueSetLookupQuerySet)()
33
+ objects = cast(AllergyIntoleranceQuerySet, AllergyIntoleranceManager())
17
34
 
18
35
  id = models.UUIDField()
19
36
  dbid = models.BigIntegerField(primary_key=True)
20
37
  created = models.DateTimeField()
21
38
  modified = models.DateTimeField()
22
39
  deleted = models.BooleanField()
23
- committer = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING, null=True)
24
- entered_in_error = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING, null=True)
40
+ committer = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING, null=True)
41
+ entered_in_error = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING, null=True)
25
42
  patient = models.ForeignKey(
26
- Patient,
43
+ "v1.Patient",
27
44
  on_delete=models.DO_NOTHING,
28
45
  related_name="allergy_intolerances",
29
46
  null=True,
@@ -46,7 +63,6 @@ class AllergyIntoleranceCoding(models.Model):
46
63
 
47
64
  class Meta:
48
65
  managed = False
49
- app_label = "canvas_sdk"
50
66
  db_table = "canvas_sdk_data_api_allergyintolerancecoding_001"
51
67
 
52
68
  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,10 +1,8 @@
1
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, Self
2
2
 
3
3
  from django.db import models
4
4
 
5
5
  from canvas_sdk.v1.data.base import ValueSetTimeframeLookupQuerySet
6
- from canvas_sdk.v1.data.note import Note
7
- from canvas_sdk.v1.data.patient import Patient
8
6
  from canvas_sdk.value_set.value_set import CodeConstants
9
7
 
10
8
  if TYPE_CHECKING:
@@ -14,7 +12,7 @@ if TYPE_CHECKING:
14
12
  class BillingLineItemQuerySet(ValueSetTimeframeLookupQuerySet):
15
13
  """A class that adds functionality to filter BillingLineItem objects."""
16
14
 
17
- def find(self, value_set: type["ValueSet"]) -> models.QuerySet:
15
+ def find(self, value_set: type["ValueSet"]) -> Self:
18
16
  """
19
17
  This method is overridden to use for BillingLineItem CPT codes.
20
18
  The codes are saved as string values in the BillingLineItem.cpt field,
@@ -36,7 +34,6 @@ class BillingLineItem(models.Model):
36
34
 
37
35
  class Meta:
38
36
  managed = False
39
- app_label = "canvas_sdk"
40
37
  db_table = "canvas_sdk_data_api_billinglineitem_001"
41
38
 
42
39
  # objects = BillingLineItemQuerySet.as_manager()
@@ -47,10 +44,10 @@ class BillingLineItem(models.Model):
47
44
  created = models.DateTimeField()
48
45
  modified = models.DateTimeField()
49
46
  note = models.ForeignKey(
50
- Note, on_delete=models.DO_NOTHING, related_name="billing_line_items", null=True
47
+ "v1.Note", on_delete=models.DO_NOTHING, related_name="billing_line_items", null=True
51
48
  )
52
49
  patient = models.ForeignKey(
53
- Patient, on_delete=models.DO_NOTHING, related_name="billing_line_items", null=True
50
+ "v1.Patient", on_delete=models.DO_NOTHING, related_name="billing_line_items", null=True
54
51
  )
55
52
  cpt = models.CharField()
56
53
  charge = models.DecimalField()