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.
- {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/METADATA +2 -2
- {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/RECORD +87 -52
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +6 -3
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py +4 -1
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +1 -1
- canvas_cli/utils/validators/manifest_schema.py +9 -2
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +18 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +48 -0
- canvas_sdk/commands/tests/protocol/tests.py +3 -1
- canvas_sdk/commands/tests/test_utils.py +76 -18
- canvas_sdk/effects/banner_alert/tests.py +41 -20
- canvas_sdk/effects/launch_modal.py +14 -3
- canvas_sdk/events/base.py +1 -3
- canvas_sdk/handlers/action_button.py +33 -13
- canvas_sdk/handlers/application.py +1 -1
- canvas_sdk/handlers/cron_task.py +1 -1
- canvas_sdk/protocols/clinical_quality_measure.py +1 -1
- canvas_sdk/templates/__init__.py +3 -0
- canvas_sdk/templates/tests/__init__.py +0 -0
- canvas_sdk/templates/tests/test_utils.py +43 -0
- canvas_sdk/templates/utils.py +44 -0
- canvas_sdk/v1/apps.py +7 -0
- canvas_sdk/v1/data/__init__.py +98 -5
- canvas_sdk/v1/data/allergy_intolerance.py +25 -9
- canvas_sdk/v1/data/appointment.py +56 -0
- canvas_sdk/v1/data/assessment.py +40 -0
- canvas_sdk/v1/data/base.py +35 -22
- canvas_sdk/v1/data/billing.py +4 -7
- canvas_sdk/v1/data/care_team.py +60 -0
- canvas_sdk/v1/data/command.py +8 -10
- canvas_sdk/v1/data/common.py +53 -0
- canvas_sdk/v1/data/condition.py +22 -10
- canvas_sdk/v1/data/coverage.py +294 -0
- canvas_sdk/v1/data/detected_issue.py +5 -9
- canvas_sdk/v1/data/device.py +4 -8
- canvas_sdk/v1/data/imaging.py +12 -17
- canvas_sdk/v1/data/lab.py +41 -31
- canvas_sdk/v1/data/medication.py +16 -10
- canvas_sdk/v1/data/note.py +11 -14
- canvas_sdk/v1/data/observation.py +19 -14
- canvas_sdk/v1/data/organization.py +1 -2
- canvas_sdk/v1/data/patient.py +140 -2
- canvas_sdk/v1/data/practicelocation.py +2 -4
- canvas_sdk/v1/data/protocol_override.py +21 -8
- canvas_sdk/v1/data/questionnaire.py +20 -17
- canvas_sdk/v1/data/staff.py +5 -7
- canvas_sdk/v1/data/task.py +5 -11
- canvas_sdk/v1/data/user.py +0 -1
- canvas_sdk/v1/models.py +4 -0
- canvas_sdk/value_set/hcc2018.py +55369 -0
- plugin_runner/plugin_installer.py +1 -1
- plugin_runner/plugin_runner.py +5 -25
- plugin_runner/sandbox.py +133 -9
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/CANVAS_MANIFEST.json +38 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/README.md +11 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/my_protocol.py +33 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__init__.py +3 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py +6 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py +5 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py +4 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/base.py +10 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py +18 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/base.py +10 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/my_protocol.py +18 -0
- plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +47 -0
- plugin_runner/tests/fixtures/plugins/test_render_template/README.md +11 -0
- plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +43 -0
- plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +10 -0
- plugin_runner/tests/test_application.py +9 -9
- plugin_runner/tests/test_plugin_installer.py +12 -1
- plugin_runner/tests/test_plugin_runner.py +159 -66
- plugin_runner/tests/test_sandbox.py +26 -14
- settings.py +13 -1
- canvas_sdk/models/__init__.py +0 -8
- {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
|
63
|
+
if not self.BUTTON_LOCATION:
|
|
39
64
|
return []
|
|
40
65
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if self.
|
|
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
|
-
|
|
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
|
|
canvas_sdk/handlers/cron_task.py
CHANGED
|
@@ -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 []
|
|
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
canvas_sdk/v1/data/__init__.py
CHANGED
|
@@ -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
|
|
6
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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 =
|
|
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()
|
canvas_sdk/v1/data/base.py
CHANGED
|
@@ -10,25 +10,12 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from canvas_sdk.value_set.value_set import ValueSet
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
14
|
-
"""A manager for
|
|
13
|
+
class BaseModelManager(models.Manager):
|
|
14
|
+
"""A base manager for models."""
|
|
15
15
|
|
|
16
|
-
def get_queryset(self) ->
|
|
16
|
+
def get_queryset(self) -> models.QuerySet:
|
|
17
17
|
"""Return a queryset that filters out deleted objects."""
|
|
18
|
-
|
|
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) ->
|
|
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"]) ->
|
|
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") ->
|
|
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
|
|
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(
|
|
180
|
+
class ValueSetLookupByNameQuerySet(BaseQuerySet, ValueSetLookupByNameQuerySetMixin):
|
|
168
181
|
"""A class that includes methods for looking up value sets by name."""
|
|
169
182
|
|
|
170
183
|
pass
|
canvas_sdk/v1/data/billing.py
CHANGED
|
@@ -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"]) ->
|
|
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()
|