canvas 0.51.0__py3-none-any.whl → 0.52.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvas
3
- Version: 0.51.0
3
+ Version: 0.52.0
4
4
  Summary: SDK to customize event-driven actions in your Canvas instance
5
5
  Author-email: Canvas Team <engineering@canvasmedical.com>
6
6
  License-Expression: MIT
@@ -192,7 +192,7 @@ canvas_sdk/effects/protocol_card/protocol_card.py,sha256=kYkyl0gUi147VJUm-mgrrtR
192
192
  canvas_sdk/effects/surescripts/__init__.py,sha256=vD-g2zW8HzGMfxcaure5ZhrQNP72iIDROAsCrzLH3y4,349
193
193
  canvas_sdk/effects/surescripts/surescripts_messages.py,sha256=7Cd_93SJVCgrneXadubUSjLQwS0N2lNp2ISH_6a24cs,2746
194
194
  canvas_sdk/effects/task/__init__.py,sha256=KlIinP5QbWMkoHGWPTg6Wi-TolPWW7TnEMVZ-h0Z3YE,168
195
- canvas_sdk/effects/task/task.py,sha256=Cbyaww6qja9RoYRHbBindgpRAGqYOLbL_FISu560XIY,3034
195
+ canvas_sdk/effects/task/task.py,sha256=Q_MGMCNbNOJf9xf9upMhwwLNxlozGzDqtUI7gmyIiHw,4350
196
196
  canvas_sdk/effects/widgets/__init__.py,sha256=vuMvOTrHNC-6tiIpdJ3Ct59EeBd9RUFZeFN9-vMHr4c,83
197
197
  canvas_sdk/effects/widgets/portal_widget.py,sha256=5K8gqbS9OoF6P7oKvsWEOV9ij448VH3JEQu8YcIYwOQ,2441
198
198
  canvas_sdk/events/__init__.py,sha256=eIJeJL52DUc1It1Dm6E8TrLzW-p2buKrsxEsy1UhYpU,270
@@ -224,7 +224,7 @@ canvas_sdk/utils/plugins.py,sha256=xxJUeVJBzfQWhlD1kO7HlFWxDAl01kkvbsf07Z08dOM,1
224
224
  canvas_sdk/v1/__init__.py,sha256=YYXr5tEQlnwMgTXJbOG963tKSPiUOM4mUkX8wuiqp7U,17
225
225
  canvas_sdk/v1/apps.py,sha256=4MwQfQu78oX5bUVlyxYzbt4rZODi0ZesXe_3QOsEWO8,170
226
226
  canvas_sdk/v1/models.py,sha256=LqeHZ-Hz3EWyM9vYr9uZ9rBYdMXNuuvJqbmojV8EcEM,224
227
- canvas_sdk/v1/data/__init__.py,sha256=FL8sB_g-YgBv769YU10f1oy9xmwiXyG6J_RwQSyrO4k,5099
227
+ canvas_sdk/v1/data/__init__.py,sha256=z2v3leRHvkPEjULJq2OxlTzURXI7-o9r1z2QokUUaP4,5281
228
228
  canvas_sdk/v1/data/allergy_intolerance.py,sha256=KXfU7UqTdRfHUFrP8yncQGMvlRPEt2K6TrC_y7JYgiE,2453
229
229
  canvas_sdk/v1/data/appointment.py,sha256=UXvn6wYEgsphhTQaFbFg-UE7Bumy_UR-AQFnsSrL5Dk,2658
230
230
  canvas_sdk/v1/data/assessment.py,sha256=vnVoaKWoJGCfaYn6WilxpI5cTgkvM647hFIuTyi_AgY,1663
@@ -236,7 +236,7 @@ canvas_sdk/v1/data/care_team.py,sha256=vQh2QS8T6JvYL8seMVpVFI9qQLpZRo6NT5lMgnSf0
236
236
  canvas_sdk/v1/data/charge_description_master.py,sha256=dyxa4Fiwts6wTyoDR-blbPZ_D95pojDLWPrNbmemx2Y,856
237
237
  canvas_sdk/v1/data/claim.py,sha256=xCuZ1SSiQ_LxUX-0GoxwKstEZwi87cnqjn8QB6m86S8,11530
238
238
  canvas_sdk/v1/data/claim_line_item.py,sha256=5WyTqlWSdQSDKyGWwRv5Dyn24_etyp8Zg_Q8aNgGL6o,5072
239
- canvas_sdk/v1/data/command.py,sha256=1_9CUk-KwUbrfXiaMjzedpouLbieSJigcIqTYiTMIuE,1848
239
+ canvas_sdk/v1/data/command.py,sha256=G7qtNypiTK274QQExKLbnij4gM71HZMs8f79VFYvIiY,1944
240
240
  canvas_sdk/v1/data/common.py,sha256=u5W3_7kXAGPe7guEBwRouKp8FMRCx9iZL6ei8QMmtwU,4880
241
241
  canvas_sdk/v1/data/compound_medication.py,sha256=a_m_hlRyZyF_c6FIIEUuX34hSslikd7RueLmjbRC12s,2239
242
242
  canvas_sdk/v1/data/condition.py,sha256=u9Vgo8h5TYhof-GYPpezpP3YdN_lfCAw4jCN4QSqpfE,2376
@@ -245,13 +245,13 @@ canvas_sdk/v1/data/detected_issue.py,sha256=k-tOX6fpzfRf9WmG7neHPEeWrpZUdTiTEqYz
245
245
  canvas_sdk/v1/data/device.py,sha256=5YnCApQR3ZprGO9zCsSP-4spwo4HVRDSTY3QCNwqYlo,1932
246
246
  canvas_sdk/v1/data/discount.py,sha256=Bqa0PcytSdqZ0MJor9EShtRUmNWGcBKtivGCmjk9Pbw,618
247
247
  canvas_sdk/v1/data/fields.py,sha256=bI7mPI_3B36ntyoVQ6vuDq8eGLoUIkwZZr14lTC-4To,1104
248
- canvas_sdk/v1/data/imaging.py,sha256=IvbuPeZxaZXmPPxJmhk4mMTdrIiMyu2UogjWB6tk3gs,4649
248
+ canvas_sdk/v1/data/imaging.py,sha256=N0OkJjmHI3jtv1w8RBhqGZGDGidNpf1ZwT0ypm1o60M,5246
249
249
  canvas_sdk/v1/data/invoice.py,sha256=F5j_Hvu-YwqhHFkp6FDui8vx6GumCAgj14YgDvfyeYU,1877
250
- canvas_sdk/v1/data/lab.py,sha256=sAfIvZfTDly-QUMVUfFDXoRjtNQbfLRJOJnL3tIjvIw,12902
250
+ canvas_sdk/v1/data/lab.py,sha256=8FPnApgcEfZ5f62387EgY6RZd8za2wps5LMzCB0_x6U,12969
251
251
  canvas_sdk/v1/data/line_item_transaction.py,sha256=OJA_3DLA1RF0v8jCgQ8SHaoqij_Z97Iatfi5i6CVEis,2406
252
252
  canvas_sdk/v1/data/medication.py,sha256=QMAUqqldJeQK9IuaoO5mWFp3IEjbqcqrxkzIgFZ4O0U,2333
253
253
  canvas_sdk/v1/data/message.py,sha256=EKc2kkWGuzrgBH3sDUa9jiNpRP7J1ZdWE6BnPhY2gvs,2268
254
- canvas_sdk/v1/data/note.py,sha256=aYLa9Hh8jwwBTZlH5y62lWrCVv0295fWOXt9m39Gaz0,8989
254
+ canvas_sdk/v1/data/note.py,sha256=ZZN0CoQbXmXxzo1cb7bpiXBcy7kLpTzZueGgdd6H95Q,9301
255
255
  canvas_sdk/v1/data/observation.py,sha256=l1EXGQ8FBBInxQthsmY1Oms9v73ni_J7SAP5M8Hmkvg,3881
256
256
  canvas_sdk/v1/data/organization.py,sha256=gt0KZC1hklwWf57vCxs2K0qPnLpMyyNFWNL7-WZ6AqI,1159
257
257
  canvas_sdk/v1/data/patient.py,sha256=g1y1Mq38obqYOMv8NaJ_WpzieO1XAjoqhGsCUG8GGBs,9183
@@ -263,7 +263,9 @@ canvas_sdk/v1/data/practicelocation.py,sha256=O-avErANUE3KwUB_vlRbTe9MywVj2jfBgc
263
263
  canvas_sdk/v1/data/protocol_override.py,sha256=zSDHngf9Pqz4irYN_BoX5wy-uJc83NFunPBJnUfS1OI,2259
264
264
  canvas_sdk/v1/data/questionnaire.py,sha256=4vQFbBjnvfXwvb73Q0ES64bnWME6P-Oy_9E9ioWAOg8,7209
265
265
  canvas_sdk/v1/data/reason_for_visit.py,sha256=diXbeGUUc_XBmIDpzubxesc6Nhj5F87zw2oYurPSE3M,669
266
- canvas_sdk/v1/data/staff.py,sha256=rHNv8Q3tX7LoNsxVrGphOSQYntxDbOaT0tBIDjnW_P0,5600
266
+ canvas_sdk/v1/data/referral.py,sha256=qVPHWqDlJoBzrYsNhP6z2NQ2Wpus_OxW8LwHYpLlM-M,3589
267
+ canvas_sdk/v1/data/service_provider.py,sha256=LQfQ0esg97NpmSYnftsWqYUJyJIQOZKmpZFNnGsxXoQ,1917
268
+ canvas_sdk/v1/data/staff.py,sha256=iVqBXwANp1qYEL39DIpveKbuqd1nw1oSDQ917xw8xgk,8302
267
269
  canvas_sdk/v1/data/task.py,sha256=9jSqr7e7ODJTs4hanWsOD-1Rb8gByJOuLta90V9V_hY,3480
268
270
  canvas_sdk/v1/data/team.py,sha256=J4s98sS7UuUw9jJCe-_7eikr1dV4SQARDzK5oxmsO_o,2832
269
271
  canvas_sdk/v1/data/user.py,sha256=14_yDFXQ38DF7quZLY5pXmL90_0wfYUTZvEkfKEAgAY,477
@@ -293,7 +295,7 @@ canvas_sdk/views/__init__.py,sha256=YYXr5tEQlnwMgTXJbOG963tKSPiUOM4mUkX8wuiqp7U,
293
295
  logger/__init__.py,sha256=uMIOf1CDdKSmBKxKwO4CmDpvnJITci76IMXENOwSHEo,85
294
296
  logger/logger.py,sha256=l5W5jn3vGQt6TM7OxOpoMy-vC7RNNA2pGnQJJgHeXWA,2068
295
297
  plugin_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
296
- plugin_runner/allowed-module-imports.json,sha256=syhNOWRtHWHHI2pati_b1i9x7zTIXMm39qOPhpcMXuI,35108
298
+ plugin_runner/allowed-module-imports.json,sha256=j6-KDxn9oJLZXfLwXBSg35znDOUPHqluxu5CcOC1bOA,35350
297
299
  plugin_runner/authentication.py,sha256=UyyhXajokVFH866dpDhoTlXS9Cg7y0sQltn0_LcwXrY,1131
298
300
  plugin_runner/aws_headers.py,sha256=wZ8584E1fTW0CdGxOCnLSF8alH27z-URcUyoc6y6ohg,2782
299
301
  plugin_runner/exceptions.py,sha256=YnRZiQVzbU3HrVlmEXLje_np99009YnhTRVHHyBCtqc,433
@@ -301,14 +303,14 @@ plugin_runner/generate_allowed_imports.py,sha256=LQuVxL_j5n0Sj-KgR4Q8D9mj0xfuDqz
301
303
  plugin_runner/installation.py,sha256=LLjtnzPk-w4go3UbXnBItJTKz1ajR_5kGQbFXTaWTFU,7693
302
304
  plugin_runner/load_all_plugins.py,sha256=4T2gW2YljhIx4xfwf1c0F_8oIbE1ubsLj0ShkHRtlVY,5847
303
305
  plugin_runner/plugin_runner.py,sha256=PqtvyUHOSvIHRW97zX_NKEjszJ1GVmETXxkZhzkZoe0,21975
304
- plugin_runner/sandbox.py,sha256=Stu_DBJNViFrwn44LuZOopXNEtt7w7YVWFcWi_P95kQ,29368
306
+ plugin_runner/sandbox.py,sha256=uFVmKLMNkOEo3lZhAcfjxKYHzUpHzaRPxfFnAhlOb50,30189
305
307
  protobufs/canvas_generated/messages/effects.proto,sha256=zTCelFeZ2ajsQPadiWFPOfqqpEB2ekSiLrdqmSd6tbY,9316
306
308
  protobufs/canvas_generated/messages/events.proto,sha256=XBMsexTQb_4ZnvRF_u4ghEfKpuHN7eU6CJJmHGM-ThU,51507
307
309
  protobufs/canvas_generated/messages/plugins.proto,sha256=oNainUPWFYQjgCX7bJEPI9_VnHC5VZduzOqgR4Q7dNM,109
308
310
  protobufs/canvas_generated/services/plugin_runner.proto,sha256=doadBKn5k4xAtOgR-q_pEvW4yzxpUaHNOowMG6CL5GY,304
309
311
  pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
310
312
  pubsub/pubsub.py,sha256=PHIvJ5SD3M-jQSYeGGSj1FuG6CvP6BQffAoGax9Uudk,1423
311
- canvas-0.51.0.dist-info/METADATA,sha256=tldZBuG1DdHgxdl4w1JrmMCf3YVcjuZn6qhacznQTnU,4645
312
- canvas-0.51.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
313
- canvas-0.51.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
314
- canvas-0.51.0.dist-info/RECORD,,
313
+ canvas-0.52.0.dist-info/METADATA,sha256=YFNhTGJ5dcp-vkquOd6o9i3bk7ENveYs-LN6R_7yzdk,4645
314
+ canvas-0.52.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
315
+ canvas-0.52.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
316
+ canvas-0.52.0.dist-info/RECORD,,
@@ -1,6 +1,9 @@
1
1
  from datetime import datetime
2
- from enum import Enum
3
- from typing import Any, cast
2
+ from enum import Enum, StrEnum
3
+ from typing import Any, Self, cast
4
+ from uuid import UUID
5
+
6
+ from pydantic import model_validator
4
7
 
5
8
  from canvas_sdk.effects.base import EffectType, _BaseEffect
6
9
 
@@ -18,10 +21,17 @@ class AddTask(_BaseEffect):
18
21
  An Effect that will create a Task in Canvas.
19
22
  """
20
23
 
24
+ class LinkableObjectType(StrEnum):
25
+ """Types of objects that can be linked to a Task."""
26
+
27
+ REFERRAL = "REFERRAL"
28
+ IMAGING = "IMAGING"
29
+
21
30
  class Meta:
22
31
  effect_type = EffectType.CREATE_TASK
23
32
  apply_required_fields = ("title",)
24
33
 
34
+ id: str | UUID | None = None
25
35
  assignee_id: str | None = None
26
36
  team_id: str | None = None
27
37
  patient_id: str | None = None
@@ -29,11 +39,28 @@ class AddTask(_BaseEffect):
29
39
  due: datetime | None = None
30
40
  status: TaskStatus = TaskStatus.OPEN
31
41
  labels: list[str] = []
42
+ linked_object_id: str | UUID | None = None
43
+ linked_object_type: LinkableObjectType | None = None
44
+
45
+ @model_validator(mode="after")
46
+ def check_needed_together_fields(self) -> Self:
47
+ """Check that linked_object_id and linked_object_type are set together."""
48
+ if self.linked_object_id is not None and self.linked_object_type is None:
49
+ raise ValueError(
50
+ "'linked_object_id' must be set with 'linked_object_type' if it is provided"
51
+ )
52
+ if self.linked_object_id is None and self.linked_object_type is not None:
53
+ raise ValueError(
54
+ "'linked_object_type' must be set with 'linked_object_id' if it is provided"
55
+ )
56
+
57
+ return self
32
58
 
33
59
  @property
34
60
  def values(self) -> dict[str, Any]:
35
61
  """The values for Task addition."""
36
62
  return {
63
+ "id": str(self.id) if self.id else None,
37
64
  "patient": {"id": self.patient_id},
38
65
  "due": self.due.isoformat() if self.due else None,
39
66
  "assignee": {"id": self.assignee_id},
@@ -41,6 +68,10 @@ class AddTask(_BaseEffect):
41
68
  "title": self.title,
42
69
  "status": self.status.value,
43
70
  "labels": self.labels,
71
+ "linked_object": {
72
+ "id": str(self.linked_object_id) if self.linked_object_id else None,
73
+ "type": self.linked_object_type.value if self.linked_object_type else None,
74
+ },
44
75
  }
45
76
 
46
77
 
@@ -57,12 +88,12 @@ class AddTaskComment(_BaseEffect):
57
88
  )
58
89
 
59
90
  body: str | None = None
60
- task_id: str | None = None
91
+ task_id: str | UUID | None = None
61
92
 
62
93
  @property
63
94
  def values(self) -> dict[str, Any]:
64
95
  """The values for adding a task comment."""
65
- return {"task": {"id": self.task_id}, "body": self.body}
96
+ return {"task": {"id": str(self.task_id) if self.task_id else None}, "body": self.body}
66
97
 
67
98
 
68
99
  class UpdateTask(_BaseEffect):
@@ -80,7 +80,9 @@ from .questionnaire import (
80
80
  ResponseOptionSet,
81
81
  )
82
82
  from .reason_for_visit import ReasonForVisitSettingCoding
83
- from .staff import Staff, StaffAddress, StaffContactPoint, StaffPhoto
83
+ from .referral import Referral, ReferralReport
84
+ from .service_provider import ServiceProvider
85
+ from .staff import Staff, StaffAddress, StaffContactPoint, StaffPhoto, StaffRole
84
86
  from .task import Task, TaskComment, TaskLabel, TaskTaskLabel
85
87
  from .team import Team, TeamContactPoint
86
88
  from .user import CanvasUser
@@ -172,11 +174,15 @@ __all__ = __exports__ = (
172
174
  "Questionnaire",
173
175
  "QuestionnaireQuestionMap",
174
176
  "ReasonForVisitSettingCoding",
177
+ "Referral",
178
+ "ReferralReport",
175
179
  "ResponseOption",
176
180
  "ResponseOptionSet",
181
+ "ServiceProvider",
177
182
  "Staff",
178
183
  "StaffAddress",
179
184
  "StaffPhoto",
185
+ "StaffRole",
180
186
  "StaffContactPoint",
181
187
  "Task",
182
188
  "TaskComment",
@@ -40,6 +40,9 @@ class Command(IdentifiableModel):
40
40
  """
41
41
  # TODO: Is the anchor object type enough here, or do we need a mapping? The home-app model
42
42
  # names might not exactly match the plugins model names.
43
+ if not self.anchor_object_type or not self.anchor_object_dbid:
44
+ return None
45
+
43
46
  anchor_model = apps.get_model(
44
47
  app_label=self._meta.app_label, model_name=self.anchor_object_type
45
48
  )
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  from django.db import models
2
4
 
3
5
  from canvas_sdk.v1.data.base import IdentifiableModel
@@ -7,6 +9,7 @@ from canvas_sdk.v1.data.common import (
7
9
  ReviewPatientCommunicationMethod,
8
10
  ReviewStatus,
9
11
  )
12
+ from canvas_sdk.v1.data.task import Task
10
13
 
11
14
 
12
15
  class ImagingOrder(IdentifiableModel):
@@ -30,20 +33,39 @@ class ImagingOrder(IdentifiableModel):
30
33
  patient = models.ForeignKey(
31
34
  "v1.Patient", on_delete=models.DO_NOTHING, related_name="imaging_orders", null=True
32
35
  )
33
- # TODO - uncomment when Note model is complete
34
- # note = models.ForeigneKey(Note, on_delete=models.DO_NOTHING, related_name="imaging_orders", null=True)
36
+ note = models.ForeignKey(
37
+ "v1.Note", on_delete=models.DO_NOTHING, related_name="imaging_orders", null=True
38
+ )
35
39
  imaging = models.CharField(max_length=1024)
36
- # TODO - uncomment when ServiceProvider model is complete
37
- # imaging_center = models.ForeignKey('v1.ServiceProvider', on_delete=models.DO_NOTHING, related_name="imaging_orders", null=True)
40
+ imaging_center = models.ForeignKey(
41
+ "v1.ServiceProvider", on_delete=models.DO_NOTHING, related_name="imaging_orders", null=True
42
+ )
38
43
  note_to_radiologist = models.CharField(max_length=1024)
39
44
  internal_comment = models.CharField(max_length=1024)
40
45
  status = models.CharField(choices=OrderStatus.choices, max_length=30)
41
46
  date_time_ordered = models.DateTimeField()
42
47
  priority = models.CharField(max_length=255)
43
- # TODO - uncomment when Staff model is complete
44
- # ordering_provider = models.ForeignKey('v1.Staff', on_delete=models.DO_NOTHING, related_name="imaging_orders", null=True)
48
+ ordering_provider = models.ForeignKey(
49
+ "v1.Staff", on_delete=models.DO_NOTHING, related_name="imaging_orders", null=True
50
+ )
45
51
  delegated = models.BooleanField(default=False)
46
52
 
53
+ task_ids = models.CharField(max_length=1024)
54
+
55
+ def get_task_objects(self) -> "models.QuerySet[Task]":
56
+ """Convert task IDs to Task objects."""
57
+ if self.task_ids:
58
+ task_ids = (
59
+ json.loads(self.task_ids) if isinstance(self.task_ids, str) else self.task_ids
60
+ )
61
+ return Task.objects.filter(id__in=task_ids)
62
+ return Task.objects.none()
63
+
64
+ @property
65
+ def task_list(self) -> list[Task]:
66
+ """Convenience property to get task objects."""
67
+ return list(self.get_task_objects())
68
+
47
69
 
48
70
  class ImagingReview(IdentifiableModel):
49
71
  """Model to read ImagingReview data."""
@@ -97,7 +119,14 @@ class ImagingReport(IdentifiableModel):
97
119
  patient = models.ForeignKey(
98
120
  "v1.Patient", on_delete=models.DO_NOTHING, related_name="imaging_results", null=True
99
121
  )
100
- order = models.ForeignKey(ImagingOrder, on_delete=models.DO_NOTHING, null=True)
122
+ order = models.ForeignKey(
123
+ ImagingOrder,
124
+ on_delete=models.DO_NOTHING,
125
+ related_name="results",
126
+ default=None,
127
+ blank=True,
128
+ null=True,
129
+ )
101
130
  source = models.CharField(choices=ImagingReportSource.choices, max_length=18)
102
131
  name = models.CharField(max_length=255)
103
132
  result_date = models.DateField()
canvas_sdk/v1/data/lab.py CHANGED
@@ -208,8 +208,8 @@ class LabOrder(IdentifiableModel):
208
208
  "v1.Patient", on_delete=models.DO_NOTHING, related_name="lab_orders", null=True
209
209
  )
210
210
  ontology_lab_partner = models.CharField(max_length=128)
211
- # TODO - uncomment when the Note model is finished
212
- # note = models.ForeignKey("Note", on_delete=models.DO_NOTHING, null=True)
211
+
212
+ note = models.ForeignKey("v1.Note", on_delete=models.DO_NOTHING, null=True)
213
213
  comment = models.CharField(max_length=128)
214
214
  requisition_number = models.CharField(max_length=32)
215
215
  is_patient_bill = models.BooleanField(null=True)
@@ -224,8 +224,10 @@ class LabOrder(IdentifiableModel):
224
224
  )
225
225
  courtesy_copy_number = models.CharField(max_length=32)
226
226
  courtesy_copy_text = models.CharField(max_length=64)
227
- ordering_provider = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, null=True)
228
- parent_order = models.ForeignKey("LabOrder", on_delete=models.DO_NOTHING, null=True)
227
+ ordering_provider = models.ForeignKey(
228
+ Staff, on_delete=models.DO_NOTHING, related_name="lab_orders", null=True
229
+ )
230
+ parent_order = models.ForeignKey("v1.LabOrder", on_delete=models.DO_NOTHING, null=True)
229
231
  healthgorilla_id = models.CharField(max_length=40)
230
232
  manual_processing_status = models.CharField(
231
233
  choices=ManualProcessingStatus.choices, null=True, max_length=16
@@ -233,6 +235,8 @@ class LabOrder(IdentifiableModel):
233
235
  manual_processing_comment = models.TextField(null=True)
234
236
  labcorp_abn_url = models.URLField()
235
237
 
238
+ reports = models.ManyToManyField("v1.LabReport", through="v1.LabTest")
239
+
236
240
 
237
241
  class LabOrderReason(Model):
238
242
  """A class representing a lab order reason."""
@@ -2,6 +2,7 @@ from django.contrib.postgres.fields import ArrayField
2
2
  from django.db import models
3
3
 
4
4
  from canvas_sdk.v1.data.base import IdentifiableModel
5
+ from canvas_sdk.v1.data.claim import Claim
5
6
 
6
7
 
7
8
  class NoteTypeCategories(models.TextChoices):
@@ -183,6 +184,13 @@ class Note(IdentifiableModel):
183
184
  datetime_of_service = models.DateTimeField()
184
185
  place_of_service = models.CharField(max_length=255)
185
186
 
187
+ def get_claim(self) -> Claim | None:
188
+ """
189
+ Get the most recent claim for this note.
190
+ Returns the latest claim ordered by created date, or None if no claims exist.
191
+ """
192
+ return self.claims.order_by("-created").first()
193
+
186
194
 
187
195
  class NoteStateChangeEvent(IdentifiableModel):
188
196
  """NoteStateChangeEvent."""
@@ -209,7 +217,7 @@ class CurrentNoteStateEvent(IdentifiableModel):
209
217
  db_table = "canvas_sdk_data_current_note_state_001"
210
218
 
211
219
  state = models.CharField(choices=NoteStates.choices, max_length=3)
212
- note = models.ForeignKey("v1.Note", on_delete=models.DO_NOTHING, related_name="+")
220
+ note = models.ForeignKey("v1.Note", on_delete=models.DO_NOTHING, related_name="current_state")
213
221
 
214
222
  def editable(self) -> bool:
215
223
  """Returns a boolean to indicate if the related note can be edited."""
@@ -0,0 +1,102 @@
1
+ import json
2
+
3
+ from django.db import models
4
+
5
+ from canvas_sdk.v1.data.base import IdentifiableModel
6
+ from canvas_sdk.v1.data.task import Task
7
+
8
+
9
+ class Referral(IdentifiableModel):
10
+ """Referral."""
11
+
12
+ class Meta:
13
+ db_table = "canvas_sdk_data_api_referral_001"
14
+
15
+ created = models.DateTimeField()
16
+ modified = models.DateTimeField()
17
+ originator = models.ForeignKey(
18
+ "v1.CanvasUser", on_delete=models.DO_NOTHING, null=True, related_name="+"
19
+ )
20
+ deleted = models.BooleanField()
21
+ committer = models.ForeignKey(
22
+ "v1.CanvasUser", on_delete=models.DO_NOTHING, null=True, related_name="+"
23
+ )
24
+ entered_in_error = models.ForeignKey(
25
+ "v1.CanvasUser", on_delete=models.DO_NOTHING, null=True, related_name="+"
26
+ )
27
+ patient = models.ForeignKey("v1.Patient", on_delete=models.DO_NOTHING)
28
+ note = models.ForeignKey("v1.Note", on_delete=models.DO_NOTHING)
29
+ service_provider = models.ForeignKey(
30
+ "v1.ServiceProvider",
31
+ on_delete=models.CASCADE,
32
+ related_name="referrals",
33
+ null=True,
34
+ blank=True,
35
+ )
36
+ assessments = models.ManyToManyField("v1.Assessment", related_name="referrals", blank=True)
37
+ clinical_question = models.CharField(max_length=50)
38
+ priority = models.CharField(max_length=255)
39
+ include_visit_note = models.BooleanField()
40
+ notes = models.TextField()
41
+ date_referred = models.DateTimeField()
42
+ forwarded = models.BooleanField()
43
+ internal_comment = models.TextField()
44
+ internal_task_comment = models.OneToOneField(
45
+ "v1.TaskComment", on_delete=models.SET_NULL, null=True, related_name="referral"
46
+ )
47
+ ignored = models.BooleanField()
48
+
49
+ task_ids = models.CharField(max_length=1024)
50
+
51
+ def get_task_objects(self) -> "models.QuerySet[Task]":
52
+ """Convert task IDs to Task objects."""
53
+ if self.task_ids:
54
+ task_ids = (
55
+ json.loads(self.task_ids) if isinstance(self.task_ids, str) else self.task_ids
56
+ )
57
+ return Task.objects.filter(id__in=task_ids)
58
+ return Task.objects.none()
59
+
60
+ @property
61
+ def task_list(self) -> list[Task]:
62
+ """Convenience property to get task objects."""
63
+ return list(self.get_task_objects())
64
+
65
+ def __str__(self) -> str:
66
+ return f"Referral {self.id}"
67
+
68
+
69
+ class ReferralReport(IdentifiableModel):
70
+ """ReferralReport."""
71
+
72
+ class Meta:
73
+ db_table = "canvas_sdk_data_api_referralreport_001"
74
+
75
+ created = models.DateTimeField(auto_now_add=True)
76
+ modified = models.DateTimeField(auto_now=True)
77
+ originator = models.ForeignKey(
78
+ "v1.CanvasUser", on_delete=models.DO_NOTHING, null=True, related_name="+"
79
+ )
80
+
81
+ review_mode = models.CharField(max_length=2)
82
+ assigned_by = models.ForeignKey(
83
+ "v1.CanvasUser", on_delete=models.DO_NOTHING, null=True, related_name="+"
84
+ )
85
+ junked = models.BooleanField()
86
+ requires_signature = models.BooleanField()
87
+ assigned_date = models.DateTimeField(null=True)
88
+ team_assigned_date = models.DateTimeField(null=True)
89
+ team = models.ForeignKey("v1.Team", on_delete=models.DO_NOTHING, null=True)
90
+ patient = models.ForeignKey(
91
+ "v1.Patient", on_delete=models.DO_NOTHING, related_name="referral_reports"
92
+ )
93
+ referral = models.ForeignKey(
94
+ Referral, on_delete=models.DO_NOTHING, related_name="reports", null=True
95
+ )
96
+ specialty = models.CharField(max_length=250)
97
+ original_date = models.DateField(null=True)
98
+ comment = models.TextField()
99
+ priority = models.BooleanField(default=False)
100
+
101
+
102
+ __exports__ = ("Referral", "ReferralReport")
@@ -0,0 +1,54 @@
1
+ from functools import cached_property
2
+
3
+ from django.db import models
4
+
5
+ from canvas_sdk.v1.data.base import IdentifiableModel
6
+
7
+
8
+ class ServiceProvider(IdentifiableModel):
9
+ """ServiceProvider."""
10
+
11
+ class Meta:
12
+ db_table = "canvas_sdk_data_data_integration_serviceprovider_001"
13
+
14
+ first_name = models.CharField(max_length=512)
15
+ # organizations won't have a last name
16
+ last_name = models.CharField(max_length=512, default="", blank=True)
17
+ business_fax = models.CharField(max_length=512, null=True, blank=True)
18
+ business_phone = models.CharField(max_length=512, null=True, blank=True)
19
+ business_address = models.CharField(max_length=512, null=True, blank=True)
20
+ specialty = models.CharField(max_length=512)
21
+ practice_name = models.CharField(max_length=512, null=True, blank=True)
22
+ notes = models.TextField(default="", null=True, blank=True)
23
+
24
+ @property
25
+ def full_name(self) -> str:
26
+ """Service provider full name."""
27
+ return f"{self.first_name} {self.last_name}"
28
+
29
+ @cached_property
30
+ def full_name_and_specialty(self) -> str:
31
+ """Service provider full name and specialty."""
32
+ name_components: list[str] = []
33
+
34
+ # Note 1: if firstName is (TBD) then insert at the end instead of the beginning
35
+ if self.first_name != "(TBD)":
36
+ name_components.append(self.first_name)
37
+
38
+ if self.first_name != self.last_name:
39
+ name_components.append(self.last_name)
40
+
41
+ if self.practice_name and self.practice_name != "(TBD)":
42
+ name_components.append(f"({self.practice_name}),")
43
+
44
+ if self.specialty not in [self.first_name, self.last_name, self.practice_name]:
45
+ name_components.append(self.specialty)
46
+
47
+ # see Note 1
48
+ if self.first_name == "(TBD)":
49
+ name_components.append(self.first_name)
50
+
51
+ return " ".join(name_components)
52
+
53
+
54
+ __exports__ = ("ServiceProvider",)
@@ -1,5 +1,8 @@
1
+ from functools import cached_property
2
+
1
3
  from django.contrib.postgres.fields import ArrayField
2
4
  from django.db import models
5
+ from django.db.models.enums import TextChoices
3
6
  from timezone_utils.fields import TimeZoneField
4
7
 
5
8
  from canvas_sdk.v1.data.base import IdentifiableModel, Model
@@ -82,6 +85,47 @@ class Staff(Model):
82
85
  photo = self.photos.first()
83
86
  return photo.url if photo else "https://d3hn0m4rbsz438.cloudfront.net/avatar1.png"
84
87
 
88
+ @cached_property
89
+ def full_name(self) -> str:
90
+ """Return Staff's first + last name."""
91
+ return f"{self.first_name} {self.last_name}"
92
+
93
+ @cached_property
94
+ def top_clinical_role(self) -> "StaffRole | None":
95
+ """Returns the topmost clinical role to assist in determining privilege levels.
96
+
97
+ Returns:
98
+ StaffRole | None: the topmost clinical role of the staff member.
99
+ """
100
+ roles = [
101
+ role
102
+ for role in self.roles.all()
103
+ if role.domain in StaffRole.RoleDomain.clinical_domains()
104
+ ]
105
+
106
+ if not roles:
107
+ return None
108
+
109
+ return roles[0]
110
+
111
+ @cached_property
112
+ def top_role_abbreviation(self) -> str | None:
113
+ """Returns the abbreviation string for the topmost role that the Staff object has.
114
+
115
+ Returns:
116
+ Optional[str]: The abbreviation for the topmost role, if available.
117
+ """
118
+ return self.top_clinical_role.public_abbreviation if self.top_clinical_role else None
119
+
120
+ @cached_property
121
+ def credentialed_name(self) -> str:
122
+ """Returns the full name of the staff member, suffixed with their topmost credential.
123
+
124
+ Returns:
125
+ str: The credentialed full name of the staff member.
126
+ """
127
+ return " ".join(filter(bool, [self.full_name, self.top_role_abbreviation or ""]))
128
+
85
129
 
86
130
  class StaffContactPoint(IdentifiableModel):
87
131
  """StaffContactPoint."""
@@ -136,4 +180,35 @@ class StaffPhoto(Model):
136
180
  title = models.CharField(max_length=255, blank=True, default="")
137
181
 
138
182
 
139
- __exports__ = ("Staff", "StaffContactPoint", "StaffAddress", "StaffPhoto")
183
+ class StaffRole(Model):
184
+ """StaffRole."""
185
+
186
+ class Meta:
187
+ db_table = "canvas_sdk_data_api_staffrole_001"
188
+
189
+ class RoleDomain(TextChoices):
190
+ CLINICAL = "CLI", "Clinical"
191
+ ADMINISTRATIVE = "ADM", "Administrative"
192
+ HYBRID = "HYB", "Hybrid"
193
+
194
+ @staticmethod
195
+ def clinical_domains() -> list["StaffRole.RoleDomain"]:
196
+ """Return a list of clinical role domains."""
197
+ return [StaffRole.RoleDomain.CLINICAL, StaffRole.RoleDomain.HYBRID]
198
+
199
+ class RoleType(TextChoices):
200
+ NON_LICENSED = "NON-LICENSED", "Non-Licensed"
201
+ LICENSED = "LICENSED", "Licensed"
202
+ PROVIDER = "PROVIDER", "Provider"
203
+
204
+ staff = models.ForeignKey(Staff, on_delete=models.CASCADE, related_name="roles")
205
+ internal_code = models.CharField(max_length=10)
206
+ public_abbreviation = models.CharField(max_length=10, default="", blank=True)
207
+ domain = models.CharField(max_length=3, choices=RoleDomain.choices, db_index=True)
208
+ name = models.CharField(max_length=50)
209
+ domain_privilege_level = models.IntegerField(default=0)
210
+ permissions = models.JSONField(default=dict, blank=True, null=True)
211
+ role_type = models.CharField(max_length=50, choices=RoleType.choices, blank=True)
212
+
213
+
214
+ __exports__ = ("Staff", "StaffContactPoint", "StaffAddress", "StaffPhoto", "StaffRole")
@@ -563,12 +563,16 @@
563
563
  "Questionnaire",
564
564
  "QuestionnaireQuestionMap",
565
565
  "ReasonForVisitSettingCoding",
566
+ "Referral",
567
+ "ReferralReport",
566
568
  "ResponseOption",
567
569
  "ResponseOptionSet",
570
+ "ServiceProvider",
568
571
  "Staff",
569
572
  "StaffAddress",
570
573
  "StaffContactPoint",
571
574
  "StaffPhoto",
575
+ "StaffRole",
572
576
  "Task",
573
577
  "TaskComment",
574
578
  "TaskLabel",
@@ -796,11 +800,19 @@
796
800
  "canvas_sdk.v1.data.reason_for_visit": [
797
801
  "ReasonForVisitSettingCoding"
798
802
  ],
803
+ "canvas_sdk.v1.data.referral": [
804
+ "Referral",
805
+ "ReferralReport"
806
+ ],
807
+ "canvas_sdk.v1.data.service_provider": [
808
+ "ServiceProvider"
809
+ ],
799
810
  "canvas_sdk.v1.data.staff": [
800
811
  "Staff",
801
812
  "StaffAddress",
802
813
  "StaffContactPoint",
803
- "StaffPhoto"
814
+ "StaffPhoto",
815
+ "StaffRole"
804
816
  ],
805
817
  "canvas_sdk.v1.data.task": [
806
818
  "EventType",
plugin_runner/sandbox.py CHANGED
@@ -4,6 +4,7 @@ import ast
4
4
  import builtins
5
5
  import importlib
6
6
  import json
7
+ import operator
7
8
  import sys
8
9
  import types
9
10
  from _ast import AnnAssign
@@ -196,6 +197,8 @@ THIRD_PARTY_MODULES = {
196
197
  "BigIntegerField",
197
198
  "Case",
198
199
  "CharField",
200
+ "Count",
201
+ "F",
199
202
  "IntegerField",
200
203
  "Model", # remove when hyperscribe no longer needs it
201
204
  "Q",
@@ -264,6 +267,34 @@ def _unrestricted(_ob: Any, *args: Any, **kwargs: Any) -> Any:
264
267
  return _ob
265
268
 
266
269
 
270
+ def _inplacevar(op: Any, val: Any, expr: Any) -> Any:
271
+ """
272
+ Apply the specified operation to the value and expression.
273
+
274
+ NOTE: Does not yet support += concatenation for sequences.
275
+ """
276
+ ops = {
277
+ "-=": operator.isub,
278
+ "@=": operator.imatmul,
279
+ "**=": operator.ipow,
280
+ "*=": operator.imul,
281
+ "//=": operator.ifloordiv,
282
+ "/=": operator.itruediv,
283
+ "&=": operator.iand,
284
+ "%=": operator.imod,
285
+ "^=": operator.ixor,
286
+ "+=": operator.iadd,
287
+ "<<=": operator.ilshift,
288
+ ">>=": operator.irshift,
289
+ "|=": operator.ior,
290
+ }
291
+
292
+ if op not in ops:
293
+ raise ValueError(f"Invalid inplace operation: {op}")
294
+
295
+ return ops[op](val, expr)
296
+
297
+
267
298
  def _apply(_ob: Any, *args: Any, **kwargs: Any) -> Any:
268
299
  """Call the bound method with args, support calling super().__init__()."""
269
300
  return _ob(*args, **kwargs)
@@ -614,6 +645,7 @@ class Sandbox:
614
645
  "dict": builtins.dict,
615
646
  "enumerate": builtins.enumerate,
616
647
  "filter": builtins.filter,
648
+ "getattr": self._safe_getattr,
617
649
  "hasattr": builtins.hasattr,
618
650
  "iter": builtins.iter,
619
651
  "list": builtins.list,
@@ -634,7 +666,7 @@ class Sandbox:
634
666
  "_getattr_": self._safe_getattr,
635
667
  "_getitem_": self._safe_getitem,
636
668
  "_getiter_": _unrestricted,
637
- "_inplacevar_": _unrestricted,
669
+ "_inplacevar_": _inplacevar,
638
670
  "_iter_unpack_sequence_": guarded_iter_unpack_sequence,
639
671
  "_print_": PrintCollector,
640
672
  "_unpack_sequence_": guarded_unpack_sequence,