canvas 0.72.0__py3-none-any.whl → 0.73.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.72.0.dist-info → canvas-0.73.0.dist-info}/METADATA +1 -1
- {canvas-0.72.0.dist-info → canvas-0.73.0.dist-info}/RECORD +25 -21
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +8 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +20 -0
- canvas_sdk/effects/fax/__init__.py +3 -0
- canvas_sdk/effects/fax/base.py +77 -0
- canvas_sdk/effects/fax/note.py +42 -0
- canvas_sdk/effects/note/__init__.py +8 -1
- canvas_sdk/effects/note/appointment.py +130 -4
- canvas_sdk/effects/task/__init__.py +2 -1
- canvas_sdk/effects/task/task.py +30 -0
- canvas_sdk/test_utils/factories/__init__.py +12 -0
- canvas_sdk/test_utils/factories/task.py +66 -0
- canvas_sdk/v1/data/__init__.py +9 -2
- canvas_sdk/v1/data/appointment.py +17 -1
- canvas_sdk/v1/data/facility.py +1 -0
- canvas_sdk/v1/data/task.py +16 -0
- plugin_runner/allowed-module-imports.json +13 -0
- plugin_runner/plugin_runner.py +4 -0
- protobufs/canvas_generated/messages/effects.proto +5 -0
- protobufs/canvas_generated/messages/events.proto +11 -0
- {canvas-0.72.0.dist-info → canvas-0.73.0.dist-info}/WHEEL +0 -0
- {canvas-0.72.0.dist-info → canvas-0.73.0.dist-info}/entry_points.txt +0 -0
|
@@ -93,12 +93,16 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
93
93
|
DETECTED_ISSUE_UPDATED: _ClassVar[EventType]
|
|
94
94
|
TASK_CLOSED: _ClassVar[EventType]
|
|
95
95
|
TASK_COMPLETED: _ClassVar[EventType]
|
|
96
|
+
TASK_METADATA_CREATED: _ClassVar[EventType]
|
|
97
|
+
TASK_METADATA_UPDATED: _ClassVar[EventType]
|
|
96
98
|
DETECTED_ISSUE_EVIDENCE_CREATED: _ClassVar[EventType]
|
|
97
99
|
DETECTED_ISSUE_EVIDENCE_UPDATED: _ClassVar[EventType]
|
|
98
100
|
STAFF_ACTIVATED: _ClassVar[EventType]
|
|
99
101
|
STAFF_DEACTIVATED: _ClassVar[EventType]
|
|
100
102
|
COMPOUND_MEDICATION_CREATED: _ClassVar[EventType]
|
|
101
103
|
COMPOUND_MEDICATION_UPDATED: _ClassVar[EventType]
|
|
104
|
+
APPOINTMENT_LABEL_ADDED: _ClassVar[EventType]
|
|
105
|
+
APPOINTMENT_LABEL_REMOVED: _ClassVar[EventType]
|
|
102
106
|
PRE_COMMAND_ORIGINATE: _ClassVar[EventType]
|
|
103
107
|
POST_COMMAND_ORIGINATE: _ClassVar[EventType]
|
|
104
108
|
PRE_COMMAND_UPDATE: _ClassVar[EventType]
|
|
@@ -142,6 +146,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
142
146
|
ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__POST_SEARCH: _ClassVar[EventType]
|
|
143
147
|
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
|
|
144
148
|
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
|
|
149
|
+
ADJUST_PRESCRIPTION__PRESCRIBER__PRE_SEARCH: _ClassVar[EventType]
|
|
150
|
+
ADJUST_PRESCRIPTION__PRESCRIBER__POST_SEARCH: _ClassVar[EventType]
|
|
145
151
|
ALLERGY_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
146
152
|
ALLERGY_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
147
153
|
ALLERGY_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -523,6 +529,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
523
529
|
PRESCRIBE__PHARMACY__POST_SEARCH: _ClassVar[EventType]
|
|
524
530
|
PRESCRIBE__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
|
|
525
531
|
PRESCRIBE__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
|
|
532
|
+
PRESCRIBE__PRESCRIBER__PRE_SEARCH: _ClassVar[EventType]
|
|
533
|
+
PRESCRIBE__PRESCRIBER__POST_SEARCH: _ClassVar[EventType]
|
|
526
534
|
QUESTIONNAIRE_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
527
535
|
QUESTIONNAIRE_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
528
536
|
QUESTIONNAIRE_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -599,6 +607,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
599
607
|
REFILL__PHARMACY__POST_SEARCH: _ClassVar[EventType]
|
|
600
608
|
REFILL__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
|
|
601
609
|
REFILL__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
|
|
610
|
+
REFILL__PRESCRIBER__PRE_SEARCH: _ClassVar[EventType]
|
|
611
|
+
REFILL__PRESCRIBER__POST_SEARCH: _ClassVar[EventType]
|
|
602
612
|
REMOVE_ALLERGY_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
603
613
|
REMOVE_ALLERGY_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
604
614
|
REMOVE_ALLERGY_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -1011,12 +1021,16 @@ DETECTED_ISSUE_CREATED: EventType
|
|
|
1011
1021
|
DETECTED_ISSUE_UPDATED: EventType
|
|
1012
1022
|
TASK_CLOSED: EventType
|
|
1013
1023
|
TASK_COMPLETED: EventType
|
|
1024
|
+
TASK_METADATA_CREATED: EventType
|
|
1025
|
+
TASK_METADATA_UPDATED: EventType
|
|
1014
1026
|
DETECTED_ISSUE_EVIDENCE_CREATED: EventType
|
|
1015
1027
|
DETECTED_ISSUE_EVIDENCE_UPDATED: EventType
|
|
1016
1028
|
STAFF_ACTIVATED: EventType
|
|
1017
1029
|
STAFF_DEACTIVATED: EventType
|
|
1018
1030
|
COMPOUND_MEDICATION_CREATED: EventType
|
|
1019
1031
|
COMPOUND_MEDICATION_UPDATED: EventType
|
|
1032
|
+
APPOINTMENT_LABEL_ADDED: EventType
|
|
1033
|
+
APPOINTMENT_LABEL_REMOVED: EventType
|
|
1020
1034
|
PRE_COMMAND_ORIGINATE: EventType
|
|
1021
1035
|
POST_COMMAND_ORIGINATE: EventType
|
|
1022
1036
|
PRE_COMMAND_UPDATE: EventType
|
|
@@ -1060,6 +1074,8 @@ ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__PRE_SEARCH: EventType
|
|
|
1060
1074
|
ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__POST_SEARCH: EventType
|
|
1061
1075
|
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
|
|
1062
1076
|
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__POST_SEARCH: EventType
|
|
1077
|
+
ADJUST_PRESCRIPTION__PRESCRIBER__PRE_SEARCH: EventType
|
|
1078
|
+
ADJUST_PRESCRIPTION__PRESCRIBER__POST_SEARCH: EventType
|
|
1063
1079
|
ALLERGY_COMMAND__PRE_ORIGINATE: EventType
|
|
1064
1080
|
ALLERGY_COMMAND__POST_ORIGINATE: EventType
|
|
1065
1081
|
ALLERGY_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1441,6 +1457,8 @@ PRESCRIBE__PHARMACY__PRE_SEARCH: EventType
|
|
|
1441
1457
|
PRESCRIBE__PHARMACY__POST_SEARCH: EventType
|
|
1442
1458
|
PRESCRIBE__SUPERVISING_PROVIDER__POST_SEARCH: EventType
|
|
1443
1459
|
PRESCRIBE__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
|
|
1460
|
+
PRESCRIBE__PRESCRIBER__PRE_SEARCH: EventType
|
|
1461
|
+
PRESCRIBE__PRESCRIBER__POST_SEARCH: EventType
|
|
1444
1462
|
QUESTIONNAIRE_COMMAND__PRE_ORIGINATE: EventType
|
|
1445
1463
|
QUESTIONNAIRE_COMMAND__POST_ORIGINATE: EventType
|
|
1446
1464
|
QUESTIONNAIRE_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1517,6 +1535,8 @@ REFILL__PHARMACY__PRE_SEARCH: EventType
|
|
|
1517
1535
|
REFILL__PHARMACY__POST_SEARCH: EventType
|
|
1518
1536
|
REFILL__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
|
|
1519
1537
|
REFILL__SUPERVISING_PROVIDER__POST_SEARCH: EventType
|
|
1538
|
+
REFILL__PRESCRIBER__PRE_SEARCH: EventType
|
|
1539
|
+
REFILL__PRESCRIBER__POST_SEARCH: EventType
|
|
1520
1540
|
REMOVE_ALLERGY_COMMAND__PRE_ORIGINATE: EventType
|
|
1521
1541
|
REMOVE_ALLERGY_COMMAND__POST_ORIGINATE: EventType
|
|
1522
1542
|
REMOVE_ALLERGY_COMMAND__PRE_UPDATE: EventType
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic_core import InitErrorDetails
|
|
6
|
+
|
|
7
|
+
from canvas_sdk.effects import EffectType, _BaseEffect
|
|
8
|
+
from canvas_sdk.v1.data import PracticeLocation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseFaxEffect(_BaseEffect, ABC):
|
|
12
|
+
"""Base class for fax effects."""
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
effect_type = EffectType.UNKNOWN_EFFECT
|
|
16
|
+
|
|
17
|
+
recipient_name: str
|
|
18
|
+
recipient_fax_number: str
|
|
19
|
+
include_coversheet: bool = False
|
|
20
|
+
subject: str | None = None
|
|
21
|
+
comment: str | None = None
|
|
22
|
+
location_id: str | UUID | None = None
|
|
23
|
+
|
|
24
|
+
def _get_error_details(self, method: Literal["apply"]) -> list[InitErrorDetails]:
|
|
25
|
+
errors = super()._get_error_details(method)
|
|
26
|
+
|
|
27
|
+
if self.include_coversheet:
|
|
28
|
+
if not self.subject:
|
|
29
|
+
errors.append(
|
|
30
|
+
self._create_error_detail(
|
|
31
|
+
"value",
|
|
32
|
+
"subject is required when include_coversheet is True",
|
|
33
|
+
self.subject,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
if not self.comment:
|
|
37
|
+
errors.append(
|
|
38
|
+
self._create_error_detail(
|
|
39
|
+
"value",
|
|
40
|
+
"comment is required when include_coversheet is True",
|
|
41
|
+
self.comment,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
if self.location_id:
|
|
45
|
+
if not PracticeLocation.objects.filter(id=self.location_id).exists():
|
|
46
|
+
errors.append(
|
|
47
|
+
self._create_error_detail(
|
|
48
|
+
"value",
|
|
49
|
+
f"Practice Location {self.location_id} does not exist",
|
|
50
|
+
self.location_id,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
errors.append(
|
|
55
|
+
self._create_error_detail(
|
|
56
|
+
"value",
|
|
57
|
+
"location_id is required when include_coversheet is True",
|
|
58
|
+
self.location_id,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return errors
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def values(self) -> dict[str, Any]:
|
|
66
|
+
"""Return the values of the fax effect."""
|
|
67
|
+
return {
|
|
68
|
+
"recipient_name": self.recipient_name,
|
|
69
|
+
"recipient_fax_number": self.recipient_fax_number,
|
|
70
|
+
"include_coversheet": self.include_coversheet,
|
|
71
|
+
"subject": self.subject,
|
|
72
|
+
"comment": self.comment,
|
|
73
|
+
"location_id": str(self.location_id) if self.location_id else None,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__exports__ = ()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from pydantic_core import InitErrorDetails
|
|
5
|
+
|
|
6
|
+
from canvas_generated.messages.effects_pb2 import EffectType
|
|
7
|
+
from canvas_sdk.effects.fax.base import BaseFaxEffect
|
|
8
|
+
from canvas_sdk.v1.data import Note
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FaxNoteEffect(BaseFaxEffect):
|
|
12
|
+
"""Fax note effect."""
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
effect_type = EffectType.FAX_NOTE
|
|
16
|
+
|
|
17
|
+
note_id: str | UUID
|
|
18
|
+
|
|
19
|
+
def _get_error_details(self, method: Literal["apply"]) -> list[InitErrorDetails]:
|
|
20
|
+
errors = super()._get_error_details(method)
|
|
21
|
+
if self.note_id and not Note.objects.filter(id=self.note_id).exists():
|
|
22
|
+
errors.append(
|
|
23
|
+
self._create_error_detail(
|
|
24
|
+
"value",
|
|
25
|
+
f"Note with ID {self.note_id} does not exist.",
|
|
26
|
+
self.note_id,
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return errors
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def values(self) -> dict[str, str]:
|
|
34
|
+
"""Return the values of the note fax effect."""
|
|
35
|
+
values = super().values
|
|
36
|
+
return {
|
|
37
|
+
**values,
|
|
38
|
+
"note_id": str(self.note_id),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__exports__ = ()
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
from canvas_sdk.effects.note.appointment import
|
|
1
|
+
from canvas_sdk.effects.note.appointment import (
|
|
2
|
+
AddAppointmentLabel,
|
|
3
|
+
Appointment,
|
|
4
|
+
RemoveAppointmentLabel,
|
|
5
|
+
ScheduleEvent,
|
|
6
|
+
)
|
|
2
7
|
from canvas_sdk.effects.note.base import AppointmentIdentifier
|
|
3
8
|
from canvas_sdk.effects.note.note import Note
|
|
4
9
|
|
|
@@ -7,4 +12,6 @@ __all__ = __exports__ = (
|
|
|
7
12
|
"Note",
|
|
8
13
|
"Appointment",
|
|
9
14
|
"ScheduleEvent",
|
|
15
|
+
"AddAppointmentLabel",
|
|
16
|
+
"RemoveAppointmentLabel",
|
|
10
17
|
)
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Annotated, Any
|
|
3
3
|
from uuid import UUID
|
|
4
4
|
|
|
5
|
+
from django.db.models import Count
|
|
6
|
+
from pydantic import Field
|
|
5
7
|
from pydantic_core import InitErrorDetails
|
|
6
8
|
|
|
7
|
-
from canvas_sdk.
|
|
9
|
+
from canvas_sdk.base import TrackableFieldsModel
|
|
10
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
11
|
+
from canvas_sdk.effects.base import _BaseEffect
|
|
8
12
|
from canvas_sdk.effects.note.base import AppointmentABC
|
|
9
|
-
from canvas_sdk.v1.data import
|
|
13
|
+
from canvas_sdk.v1.data import Appointment as AppointmentDataModel
|
|
14
|
+
from canvas_sdk.v1.data import AppointmentLabel, NoteType, Patient
|
|
10
15
|
from canvas_sdk.v1.data.note import NoteTypeCategories
|
|
11
16
|
|
|
12
17
|
|
|
@@ -127,6 +132,7 @@ class Appointment(AppointmentABC):
|
|
|
127
132
|
appointment_note_type_id (UUID | str | None): The ID of the appointment note type.
|
|
128
133
|
meeting_link (str | None): The meeting link for the appointment, if any.
|
|
129
134
|
patient_id (str | None): The ID of the patient.
|
|
135
|
+
labels (conset[str] | None): A set of label names to apply to the appointment.
|
|
130
136
|
"""
|
|
131
137
|
|
|
132
138
|
class Meta:
|
|
@@ -135,6 +141,13 @@ class Appointment(AppointmentABC):
|
|
|
135
141
|
appointment_note_type_id: UUID | str | None = None
|
|
136
142
|
meeting_link: str | None = None
|
|
137
143
|
patient_id: str | None = None
|
|
144
|
+
labels: (
|
|
145
|
+
Annotated[
|
|
146
|
+
set[Annotated[str, Field(min_length=1, max_length=50)]],
|
|
147
|
+
Field(min_length=1, max_length=3),
|
|
148
|
+
]
|
|
149
|
+
| None
|
|
150
|
+
) = None
|
|
138
151
|
|
|
139
152
|
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
140
153
|
"""
|
|
@@ -215,8 +228,39 @@ class Appointment(AppointmentABC):
|
|
|
215
228
|
)
|
|
216
229
|
)
|
|
217
230
|
|
|
231
|
+
if method == "update" and self.labels and self.instance_id:
|
|
232
|
+
existing_count = AppointmentLabel.objects.filter(
|
|
233
|
+
appointment__id=self.instance_id
|
|
234
|
+
).count()
|
|
235
|
+
if existing_count + len(self.labels) > 3:
|
|
236
|
+
errors.append(
|
|
237
|
+
self._create_error_detail(
|
|
238
|
+
"value",
|
|
239
|
+
f"Limit reached: Only 3 appointment labels allowed. Attempted to add {len(self.labels)} label(s) to appointment with {existing_count} existing label(s).",
|
|
240
|
+
sorted(self.labels),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
218
244
|
return errors
|
|
219
245
|
|
|
246
|
+
@property
|
|
247
|
+
def values(self) -> dict:
|
|
248
|
+
"""
|
|
249
|
+
Returns a dictionary of modified attributes with type-specific transformations.
|
|
250
|
+
"""
|
|
251
|
+
values = super().values
|
|
252
|
+
# Convert labels set to list for JSON serialization
|
|
253
|
+
# This is necessary because:
|
|
254
|
+
# 1. The labels field is defined as a conset (constrained set) for validation
|
|
255
|
+
# 2. JSON cannot serialize Python sets directly - it only supports lists, dicts, strings, numbers, booleans, and None
|
|
256
|
+
# 3. When the effect payload is serialized to JSON in the base Effect class, it would fail with:
|
|
257
|
+
# "TypeError: Object of type set is not JSON serializable"
|
|
258
|
+
# 4. Converting to list maintains the same data while making it JSON-compatible
|
|
259
|
+
# 5. Sort the labels to ensure consistent ordering for tests and API responses
|
|
260
|
+
if self.labels is not None:
|
|
261
|
+
values["labels"] = sorted(self.labels)
|
|
262
|
+
return values
|
|
263
|
+
|
|
220
264
|
def cancel(self) -> Effect:
|
|
221
265
|
"""Send a CANCEL effect for the appointment."""
|
|
222
266
|
self._validate_before_effect("cancel")
|
|
@@ -230,4 +274,86 @@ class Appointment(AppointmentABC):
|
|
|
230
274
|
)
|
|
231
275
|
|
|
232
276
|
|
|
233
|
-
|
|
277
|
+
class _AppointmentLabelBase(_BaseEffect, TrackableFieldsModel):
|
|
278
|
+
"""
|
|
279
|
+
Base class for appointment label effects.
|
|
280
|
+
|
|
281
|
+
Attributes:
|
|
282
|
+
appointment_id (UUID | str): The ID of the appointment.
|
|
283
|
+
labels (conset[str]): A set of label names (1-3 labels allowed).
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
appointment_id: str
|
|
287
|
+
labels: Annotated[set[str], Field(min_length=1, max_length=3)]
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def values(self) -> dict:
|
|
291
|
+
"""The effect's values."""
|
|
292
|
+
result = {
|
|
293
|
+
"appointment_id": str(self.appointment_id),
|
|
294
|
+
"labels": sorted(self.labels),
|
|
295
|
+
}
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class AddAppointmentLabel(_AppointmentLabelBase):
|
|
300
|
+
"""
|
|
301
|
+
Effect to add one or more labels to an appointment.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
class Meta:
|
|
305
|
+
effect_type = EffectType.ADD_APPOINTMENT_LABEL
|
|
306
|
+
|
|
307
|
+
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
308
|
+
"""Validate that the appointment does not exceed the 3-label limit."""
|
|
309
|
+
errors = super()._get_error_details(method)
|
|
310
|
+
|
|
311
|
+
appointment_label_count = (
|
|
312
|
+
AppointmentDataModel.objects.filter(id=self.appointment_id)
|
|
313
|
+
.annotate(label_count=Count("labels"))
|
|
314
|
+
.values_list("label_count", flat=True)
|
|
315
|
+
.first()
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# note that appointment_label_count will be None if the appointment doesn't exist
|
|
319
|
+
# and appointment_label_count will be 0 if the appointment exists but has no labels
|
|
320
|
+
if appointment_label_count is None:
|
|
321
|
+
errors.append(
|
|
322
|
+
self._create_error_detail(
|
|
323
|
+
"value",
|
|
324
|
+
f"Appointment {self.appointment_id} does not exist",
|
|
325
|
+
self.appointment_id,
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
elif appointment_label_count + len(self.labels) > 3:
|
|
329
|
+
errors.append(
|
|
330
|
+
self._create_error_detail(
|
|
331
|
+
"value",
|
|
332
|
+
f"Limit reached: Only 3 appointment labels allowed. "
|
|
333
|
+
f"Attempted to add {len(self.labels)} label(s) to appointment with "
|
|
334
|
+
f"{appointment_label_count} existing label(s).",
|
|
335
|
+
sorted(self.labels),
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
return errors
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class RemoveAppointmentLabel(_AppointmentLabelBase):
|
|
342
|
+
"""
|
|
343
|
+
Effect to remove one or more labels from an appointment.
|
|
344
|
+
|
|
345
|
+
Attributes:
|
|
346
|
+
appointment_id (UUID | str): The ID of the appointment to remove labels from.
|
|
347
|
+
labels (list[str]): A list of label names to remove.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
class Meta:
|
|
351
|
+
effect_type = EffectType.REMOVE_APPOINTMENT_LABEL
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
__exports__ = (
|
|
355
|
+
"ScheduleEvent",
|
|
356
|
+
"Appointment",
|
|
357
|
+
"AddAppointmentLabel",
|
|
358
|
+
"RemoveAppointmentLabel",
|
|
359
|
+
)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from .task import AddTask, AddTaskComment, TaskStatus, UpdateTask
|
|
1
|
+
from .task import AddTask, AddTaskComment, TaskMetadata, TaskStatus, UpdateTask
|
|
2
2
|
|
|
3
3
|
__all__ = __exports__ = (
|
|
4
4
|
"AddTask",
|
|
5
5
|
"AddTaskComment",
|
|
6
6
|
"TaskStatus",
|
|
7
|
+
"TaskMetadata",
|
|
7
8
|
"UpdateTask",
|
|
8
9
|
)
|
canvas_sdk/effects/task/task.py
CHANGED
|
@@ -4,8 +4,11 @@ from typing import Any, Self, cast
|
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
from pydantic import model_validator
|
|
7
|
+
from pydantic_core import InitErrorDetails
|
|
7
8
|
|
|
8
9
|
from canvas_sdk.effects.base import EffectType, _BaseEffect
|
|
10
|
+
from canvas_sdk.effects.metadata import BaseMetadata
|
|
11
|
+
from canvas_sdk.v1.data import Task
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class TaskStatus(Enum):
|
|
@@ -140,9 +143,36 @@ class UpdateTask(_BaseEffect):
|
|
|
140
143
|
return value_dict
|
|
141
144
|
|
|
142
145
|
|
|
146
|
+
class TaskMetadata(BaseMetadata):
|
|
147
|
+
"""Task Metadata."""
|
|
148
|
+
|
|
149
|
+
class Meta:
|
|
150
|
+
effect_type = "TASK_METADATA"
|
|
151
|
+
|
|
152
|
+
task_id: str
|
|
153
|
+
|
|
154
|
+
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
155
|
+
"""Get the error details for the effect.
|
|
156
|
+
If task_id is not found, return an error detail.
|
|
157
|
+
"""
|
|
158
|
+
errors = super()._get_error_details(method)
|
|
159
|
+
|
|
160
|
+
if not Task.objects.filter(id=self.task_id).exists():
|
|
161
|
+
errors.append(
|
|
162
|
+
self._create_error_detail(
|
|
163
|
+
"task_id",
|
|
164
|
+
f"Task with id: {self.task_id} does not exist.",
|
|
165
|
+
self.task_id,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return errors
|
|
170
|
+
|
|
171
|
+
|
|
143
172
|
__exports__ = (
|
|
144
173
|
"AddTask",
|
|
145
174
|
"AddTaskComment",
|
|
146
175
|
"TaskStatus",
|
|
176
|
+
"TaskMetadata",
|
|
147
177
|
"UpdateTask",
|
|
148
178
|
)
|
|
@@ -19,6 +19,13 @@ from .staff import (
|
|
|
19
19
|
StaffPhotoFactory,
|
|
20
20
|
StaffRoleFactory,
|
|
21
21
|
)
|
|
22
|
+
from .task import (
|
|
23
|
+
TaskCommentFactory,
|
|
24
|
+
TaskFactory,
|
|
25
|
+
TaskLabelFactory,
|
|
26
|
+
TaskMetadataFactory,
|
|
27
|
+
TaskTaskLabelFactory,
|
|
28
|
+
)
|
|
22
29
|
from .user import CanvasUserFactory
|
|
23
30
|
|
|
24
31
|
__all__ = (
|
|
@@ -45,4 +52,9 @@ __all__ = (
|
|
|
45
52
|
"StaffLicenseFactory",
|
|
46
53
|
"StaffContactPointFactory",
|
|
47
54
|
"StaffAddressFactory",
|
|
55
|
+
"TaskCommentFactory",
|
|
56
|
+
"TaskFactory",
|
|
57
|
+
"TaskLabelFactory",
|
|
58
|
+
"TaskMetadataFactory",
|
|
59
|
+
"TaskTaskLabelFactory",
|
|
48
60
|
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import factory
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.v1.data import Task, TaskComment, TaskLabel, TaskMetadata, TaskTaskLabel
|
|
4
|
+
from canvas_sdk.v1.data.common import ColorEnum, Origin
|
|
5
|
+
from canvas_sdk.v1.data.task import TaskLabelModule, TaskStatus, TaskType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskFactory(factory.django.DjangoModelFactory[Task]):
|
|
9
|
+
"""Factory for creating Task."""
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
model = Task
|
|
13
|
+
|
|
14
|
+
patient = factory.SubFactory("canvas_sdk.test_utils.factories.PatientFactory")
|
|
15
|
+
creator = factory.SubFactory("canvas_sdk.test_utils.factories.StaffFactory")
|
|
16
|
+
assignee = factory.SubFactory("canvas_sdk.test_utils.factories.StaffFactory")
|
|
17
|
+
task_type = TaskType.TASK
|
|
18
|
+
tag = factory.Faker("word")
|
|
19
|
+
title = factory.Faker("sentence", nb_words=4)
|
|
20
|
+
status = TaskStatus.OPEN
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TaskMetadataFactory(factory.django.DjangoModelFactory[TaskMetadata]):
|
|
24
|
+
"""Factory for creating TaskMetadata."""
|
|
25
|
+
|
|
26
|
+
class Meta:
|
|
27
|
+
model = TaskMetadata
|
|
28
|
+
|
|
29
|
+
task = factory.SubFactory(TaskFactory)
|
|
30
|
+
key = factory.Faker("word")
|
|
31
|
+
value = factory.Faker("word")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TaskCommentFactory(factory.django.DjangoModelFactory[TaskComment]):
|
|
35
|
+
"""Factory for creating TaskComment."""
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
model = TaskComment
|
|
39
|
+
|
|
40
|
+
creator = factory.SubFactory("canvas_sdk.test_utils.factories.StaffFactory")
|
|
41
|
+
task = factory.SubFactory(TaskFactory)
|
|
42
|
+
body = factory.Faker("sentence", nb_words=10)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TaskLabelFactory(factory.django.DjangoModelFactory[TaskLabel]):
|
|
46
|
+
"""Factory for creating TaskLabel."""
|
|
47
|
+
|
|
48
|
+
class Meta:
|
|
49
|
+
model = TaskLabel
|
|
50
|
+
|
|
51
|
+
position = factory.Sequence(lambda n: n)
|
|
52
|
+
color = ColorEnum.BLUE
|
|
53
|
+
task_association = [Origin.REFERAL]
|
|
54
|
+
name = factory.Faker("word")
|
|
55
|
+
active = True
|
|
56
|
+
modules = [TaskLabelModule.TASKS]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TaskTaskLabelFactory(factory.django.DjangoModelFactory[TaskTaskLabel]):
|
|
60
|
+
"""Factory for creating TaskTaskLabel."""
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
model = TaskTaskLabel
|
|
64
|
+
|
|
65
|
+
task = factory.SubFactory(TaskFactory)
|
|
66
|
+
task_label = factory.SubFactory(TaskLabelFactory)
|
canvas_sdk/v1/data/__init__.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
from .allergy_intolerance import AllergyIntolerance, AllergyIntoleranceCoding
|
|
2
|
-
from .appointment import
|
|
2
|
+
from .appointment import (
|
|
3
|
+
Appointment,
|
|
4
|
+
AppointmentExternalIdentifier,
|
|
5
|
+
AppointmentLabel,
|
|
6
|
+
AppointmentMetadata,
|
|
7
|
+
)
|
|
3
8
|
from .assessment import Assessment
|
|
4
9
|
from .banner_alert import BannerAlert
|
|
5
10
|
from .billing import BillingLineItem, BillingLineItemModifier
|
|
@@ -103,7 +108,7 @@ from .referral import Referral, ReferralReport
|
|
|
103
108
|
from .service_provider import ServiceProvider
|
|
104
109
|
from .staff import Staff, StaffAddress, StaffContactPoint, StaffLicense, StaffPhoto, StaffRole
|
|
105
110
|
from .stop_medication_event import StopMedicationEvent
|
|
106
|
-
from .task import Task, TaskComment, TaskLabel, TaskTaskLabel
|
|
111
|
+
from .task import Task, TaskComment, TaskLabel, TaskMetadata, TaskTaskLabel
|
|
107
112
|
from .team import Team, TeamContactPoint
|
|
108
113
|
from .user import CanvasUser
|
|
109
114
|
|
|
@@ -111,6 +116,7 @@ __all__ = __exports__ = (
|
|
|
111
116
|
"Appointment",
|
|
112
117
|
"AppointmentMetadata",
|
|
113
118
|
"AppointmentExternalIdentifier",
|
|
119
|
+
"AppointmentLabel",
|
|
114
120
|
"AllergyIntolerance",
|
|
115
121
|
"AllergyIntoleranceCoding",
|
|
116
122
|
"Assessment",
|
|
@@ -227,6 +233,7 @@ __all__ = __exports__ = (
|
|
|
227
233
|
"TaskComment",
|
|
228
234
|
"TaskLabel",
|
|
229
235
|
"TaskTaskLabel",
|
|
236
|
+
"TaskMetadata",
|
|
230
237
|
"Team",
|
|
231
238
|
"TeamContactPoint",
|
|
232
239
|
"Transactor",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django.db import models
|
|
2
2
|
|
|
3
|
-
from canvas_sdk.v1.data.base import IdentifiableModel, TimestampedModel
|
|
3
|
+
from canvas_sdk.v1.data.base import IdentifiableModel, Model, TimestampedModel
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class AppointmentProgressStatus(models.TextChoices):
|
|
@@ -77,6 +77,21 @@ class AppointmentExternalIdentifier(TimestampedModel, IdentifiableModel):
|
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
class AppointmentLabel(Model):
|
|
81
|
+
"""M2M for Appointment -> TaskLabels."""
|
|
82
|
+
|
|
83
|
+
class Meta:
|
|
84
|
+
db_table = "canvas_sdk_data_api_appointment_labels_001"
|
|
85
|
+
|
|
86
|
+
appointment = models.ForeignKey(Appointment, on_delete=models.DO_NOTHING, null=True)
|
|
87
|
+
task_label = models.ForeignKey(
|
|
88
|
+
"v1.TaskLabel",
|
|
89
|
+
on_delete=models.DO_NOTHING,
|
|
90
|
+
null=True,
|
|
91
|
+
db_column="userselectedtasklabel_id",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
80
95
|
class AppointmentMetadata(IdentifiableModel):
|
|
81
96
|
"""A class representing Appointment Metadata."""
|
|
82
97
|
|
|
@@ -95,4 +110,5 @@ __exports__ = (
|
|
|
95
110
|
"Appointment",
|
|
96
111
|
"AppointmentMetadata",
|
|
97
112
|
"AppointmentExternalIdentifier",
|
|
113
|
+
"AppointmentLabel",
|
|
98
114
|
)
|
canvas_sdk/v1/data/facility.py
CHANGED
|
@@ -12,6 +12,7 @@ class Facility(IdentifiableModel):
|
|
|
12
12
|
name = models.CharField(max_length=255)
|
|
13
13
|
npi_number = models.CharField(verbose_name="NPI number", max_length=10, blank=True)
|
|
14
14
|
phone_number = models.CharField(max_length=10, blank=True, default="")
|
|
15
|
+
fax_number = models.CharField(max_length=10, blank=True, default="")
|
|
15
16
|
active = models.BooleanField(default=True)
|
|
16
17
|
line1 = models.CharField(max_length=255, default="", blank=True)
|
|
17
18
|
line2 = models.CharField(max_length=255, default="", blank=True)
|
canvas_sdk/v1/data/task.py
CHANGED
|
@@ -31,6 +31,7 @@ class TaskLabelModule(models.TextChoices):
|
|
|
31
31
|
|
|
32
32
|
CLAIMS = "claims", "Claims"
|
|
33
33
|
TASKS = "tasks", "Tasks"
|
|
34
|
+
APPOINTMENTS = "appointments", "Appointments"
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class Task(TimestampedModel, IdentifiableModel):
|
|
@@ -78,6 +79,9 @@ class TaskLabel(IdentifiableModel):
|
|
|
78
79
|
db_table = "canvas_sdk_data_api_tasklabel_001"
|
|
79
80
|
|
|
80
81
|
tasks = models.ManyToManyField(Task, related_name="labels", through="TaskTaskLabel") # type: ignore[var-annotated]
|
|
82
|
+
appointments = models.ManyToManyField(
|
|
83
|
+
"v1.Appointment", related_name="labels", through="v1.AppointmentLabel"
|
|
84
|
+
)
|
|
81
85
|
position = models.IntegerField()
|
|
82
86
|
color = models.CharField(choices=ColorEnum.choices, max_length=50)
|
|
83
87
|
task_association = ArrayField(models.CharField(choices=Origin.choices, max_length=32))
|
|
@@ -96,6 +100,17 @@ class TaskTaskLabel(Model):
|
|
|
96
100
|
task = models.ForeignKey(Task, on_delete=models.DO_NOTHING, null=True)
|
|
97
101
|
|
|
98
102
|
|
|
103
|
+
class TaskMetadata(IdentifiableModel):
|
|
104
|
+
"""TaskMetadata."""
|
|
105
|
+
|
|
106
|
+
class Meta:
|
|
107
|
+
db_table = "canvas_sdk_data_api_taskmetadata_001"
|
|
108
|
+
|
|
109
|
+
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="metadata")
|
|
110
|
+
key = models.CharField(max_length=32)
|
|
111
|
+
value = models.CharField(max_length=255)
|
|
112
|
+
|
|
113
|
+
|
|
99
114
|
__exports__ = (
|
|
100
115
|
"TaskType",
|
|
101
116
|
"EventType",
|
|
@@ -105,4 +120,5 @@ __exports__ = (
|
|
|
105
120
|
"TaskComment",
|
|
106
121
|
"TaskLabel",
|
|
107
122
|
"TaskTaskLabel",
|
|
123
|
+
"TaskMetadata",
|
|
108
124
|
)
|