canvas 0.18.0__py3-none-any.whl → 0.19.1__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.18.0.dist-info → canvas-0.19.1.dist-info}/METADATA +1 -1
- {canvas-0.18.0.dist-info → canvas-0.19.1.dist-info}/RECORD +28 -18
- canvas_cli/utils/validators/manifest_schema.py +12 -0
- canvas_sdk/__init__.py +4 -2
- canvas_sdk/commands/commands/reason_for_visit.py +18 -3
- canvas_sdk/commands/tests/test_utils.py +15 -3
- canvas_sdk/commands/tests/unit/tests.py +7 -16
- canvas_sdk/questionnaires/__init__.py +3 -0
- canvas_sdk/questionnaires/tests/__init__.py +0 -0
- canvas_sdk/questionnaires/tests/test_utils.py +74 -0
- canvas_sdk/questionnaires/utils.py +117 -0
- canvas_sdk/templates/utils.py +7 -12
- canvas_sdk/utils/plugins.py +25 -0
- canvas_sdk/v1/data/__init__.py +4 -1
- canvas_sdk/v1/data/billing.py +29 -2
- canvas_sdk/v1/data/coverage.py +1 -1
- canvas_sdk/v1/data/reason_for_visit.py +22 -0
- plugin_runner/plugin_installer.py +23 -32
- plugin_runner/sandbox.py +6 -6
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/CANVAS_MANIFEST.json +52 -0
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/README.md +11 -0
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/my_protocol.py +39 -0
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/questionnaires/example_questionnaire.yml +61 -0
- plugin_runner/tests/test_plugin_runner.py +12 -12
- settings.py +22 -12
- canvas_sdk/utils/db.py +0 -17
- {canvas-0.18.0.dist-info → canvas-0.19.1.dist-info}/WHEEL +0 -0
- {canvas-0.18.0.dist-info → canvas-0.19.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
settings.py,sha256=
|
|
1
|
+
settings.py,sha256=Mk332hOSmNyLVRKLUhtc1Kll-P5Iun7vbkZ0chwnOAs,3176
|
|
2
2
|
canvas_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
canvas_cli/conftest.py,sha256=0mEjjU7_w9nq-HCvscxFJClw-EeyQYujNLLRPssksZQ,931
|
|
4
4
|
canvas_cli/main.py,sha256=L6JQkt1yxy30cA3-M9v7JD8WMW4i0M5GPr9kZetAito,2728
|
|
@@ -95,7 +95,7 @@ canvas_cli/utils/urls/__init__.py,sha256=08hlrQhQ1pKBjlIRaC0j53IkgK723jfK8-j3djv
|
|
|
95
95
|
canvas_cli/utils/urls/tests.py,sha256=opXDF2i3lXTdsKJ7ywIRzWDDzZ5KAO0JbGIR3hbJdoE,407
|
|
96
96
|
canvas_cli/utils/urls/urls.py,sha256=KwWTh5ERrEsZEvdBrZpZB71xtyWkDuglpXUbycWmBOo,798
|
|
97
97
|
canvas_cli/utils/validators/__init__.py,sha256=rBvSR2O1hWkNAnUBdcr-zUkmqT796_A61b9pnvEhwrM,113
|
|
98
|
-
canvas_cli/utils/validators/manifest_schema.py,sha256=
|
|
98
|
+
canvas_cli/utils/validators/manifest_schema.py,sha256=Bp3FFdIY7KFz1AXnQAbJa6_E1pm9PCc0CXGkwK4JdZo,4573
|
|
99
99
|
canvas_cli/utils/validators/tests.py,sha256=KhaOdbxGUK2QwL2KnycAJJkqclYQSXF7CKg-scSf0DU,1259
|
|
100
100
|
canvas_cli/utils/validators/validators.py,sha256=lrUBQ0sF7seTe_pNGsNgASdr2BGp6g-Qui58V4H9qfQ,1598
|
|
101
101
|
canvas_generated/messages/effects_pb2.py,sha256=qGIcJpjSrhLvj6Km2wMRKRPJ2IxymA0M_R9SghvilII,9821
|
|
@@ -110,7 +110,7 @@ canvas_generated/messages/plugins_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCN
|
|
|
110
110
|
canvas_generated/services/plugin_runner_pb2.py,sha256=RfAo_imYoSuoexq-1IHhMhXZgQpzq91pqugxsigL8NU,1557
|
|
111
111
|
canvas_generated/services/plugin_runner_pb2.pyi,sha256=1w-Pa4k7HtlmQAr7B6sgV64zdZplBKQKHN-S8bjwO3w,265
|
|
112
112
|
canvas_generated/services/plugin_runner_pb2_grpc.py,sha256=EzJJVkP_AZ3dwBA7OxUito0NSalRmjjg8q9TZ_P18ww,4549
|
|
113
|
-
canvas_sdk/__init__.py,sha256=
|
|
113
|
+
canvas_sdk/__init__.py,sha256=7IuwjhqIaOO5BgCfXi-B1eV6XfK9Ku5PHPWuTGeDNgU,151
|
|
114
114
|
canvas_sdk/base.py,sha256=aaWilIx70lsAE76x7QaJolyIvcFnXDGPw1UMv9t0FDw,2035
|
|
115
115
|
canvas_sdk/commands/__init__.py,sha256=cNQy1Y_Ji3cPush2hbaoXC3pbIFmobv_Vm-UG0w0hlc,2696
|
|
116
116
|
canvas_sdk/commands/base.py,sha256=ScfOo4QXTInhTf6OGLT5dEu81yhZAcgVT8qxwIKWbow,5155
|
|
@@ -132,7 +132,7 @@ canvas_sdk/commands/commands/perform.py,sha256=FpumHzPJp5pzb4F0CBW2QbSWdR_5mpib1
|
|
|
132
132
|
canvas_sdk/commands/commands/plan.py,sha256=uxjXmdNcG32R4S0CJ7fqDaaXTMiJJH9RuvwlWuztlZ0,403
|
|
133
133
|
canvas_sdk/commands/commands/prescribe.py,sha256=CWk25bQtpHb7KTNnCIPGaIGEnUQagAJ8fnbRnyPIssc,2176
|
|
134
134
|
canvas_sdk/commands/commands/questionnaire.py,sha256=Giu4stTzpddO8fH3iVyxm2lUeDwe7AmhfuIfb_0WUj4,644
|
|
135
|
-
canvas_sdk/commands/commands/reason_for_visit.py,sha256=
|
|
135
|
+
canvas_sdk/commands/commands/reason_for_visit.py,sha256=XapED7uU9yIUPkJ5p0PUboBNX7MlpM9PYfmOmAkAdpY,2242
|
|
136
136
|
canvas_sdk/commands/commands/refill.py,sha256=HC9Nl3_BkGn3HRZ-4YNT9lqIywFVom6PyJChK3_0Zyc,458
|
|
137
137
|
canvas_sdk/commands/commands/remove_allergy.py,sha256=6CXo3s63z7oqMlROExPWudQBZeSvD3x0SC_efQSb0Lw,748
|
|
138
138
|
canvas_sdk/commands/commands/review_of_systems.py,sha256=F2wXmeh5yEuWokC5I2_H-9k32PgYBUcfyLHgYVL4LPQ,276
|
|
@@ -142,13 +142,13 @@ canvas_sdk/commands/commands/task.py,sha256=p-WskHqj_q8TqllarEw73AIW6n2J7uzyIso9
|
|
|
142
142
|
canvas_sdk/commands/commands/update_diagnosis.py,sha256=fqPcKBaiRemo8Qa4H4g6ZvKM2YczVlj9FxpBGjs8J9Q,824
|
|
143
143
|
canvas_sdk/commands/commands/update_goal.py,sha256=ddvxlDnoTm5k-G5aJ-pW2fMIC-s5K8jYia9tISXsxAk,1580
|
|
144
144
|
canvas_sdk/commands/commands/vitals.py,sha256=hOuDXbNwqJbsBAvvkpjrXWqssrdQPxEfCox1NlI0h4A,3127
|
|
145
|
-
canvas_sdk/commands/tests/test_utils.py,sha256=
|
|
145
|
+
canvas_sdk/commands/tests/test_utils.py,sha256=Jrkbz2xltrjUVYRdIyNaKdq5tCtGQ4vygic7hpBp6Ek,12157
|
|
146
146
|
canvas_sdk/commands/tests/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
147
147
|
canvas_sdk/commands/tests/protocol/tests.py,sha256=e0fiJUKsfi0aYSlaTdrMHybZntC7oBdaHks9PSrMuhU,2168
|
|
148
148
|
canvas_sdk/commands/tests/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
149
149
|
canvas_sdk/commands/tests/schema/tests.py,sha256=rSu146RouMrPLv0G-E6QtgQhG2vkUCCk1M3Baa6fXBg,3701
|
|
150
150
|
canvas_sdk/commands/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
151
|
-
canvas_sdk/commands/tests/unit/tests.py,sha256=
|
|
151
|
+
canvas_sdk/commands/tests/unit/tests.py,sha256=uzQbGIgAM1PTCGJy0p8etHx0zhOB0GDVWO7WCaxZuUs,8767
|
|
152
152
|
canvas_sdk/effects/__init__.py,sha256=rcMI7IaVe8kDaYRngvlH5JPOL7FBSGIoLZB4UCN18d8,151
|
|
153
153
|
canvas_sdk/effects/base.py,sha256=znIaDNkCYbzJxf4Q80hLRYUG5H73c8IMq1UCzeBcXjw,714
|
|
154
154
|
canvas_sdk/effects/launch_modal.py,sha256=2yVY7r6hY5YYiBKae0rpS4Q0UgJPJtGlLuZGj68hm8s,1058
|
|
@@ -180,30 +180,34 @@ canvas_sdk/protocols/__init__.py,sha256=3u9zet5D4DX4V953tLCoN1xhaOhAUCwGwscMv-7I
|
|
|
180
180
|
canvas_sdk/protocols/base.py,sha256=sbm0uOk3PPfPemqBmHh2hawE5utC6no46EmvyMN8Y7Q,179
|
|
181
181
|
canvas_sdk/protocols/clinical_quality_measure.py,sha256=8cU93ah9YsPecpZR1-csAbg69oFn9a8LtjHjYMMHedw,4844
|
|
182
182
|
canvas_sdk/protocols/timeframe.py,sha256=SlTDhTy0TqPXKS9JZFeTVApQJDf8C-NIRLqFJltB17g,1148
|
|
183
|
+
canvas_sdk/questionnaires/__init__.py,sha256=WqEj8iLUjvubIzHVAXUEYQwzUFxgauAX9rPdVvJjbRQ,96
|
|
184
|
+
canvas_sdk/questionnaires/utils.py,sha256=tGTlEYWg6HJkf0zSWXgaSRsh4BW2z6dWNTTPGBU0i5Q,3569
|
|
185
|
+
canvas_sdk/questionnaires/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
186
|
+
canvas_sdk/questionnaires/tests/test_utils.py,sha256=4NCYPFSmn96wSfEB48eFfM4548sOULAX58Ln49FugEw,3032
|
|
183
187
|
canvas_sdk/templates/__init__.py,sha256=crz8FE6yoCgwTrqosLDHM7cOiVdhWgWz0l0J--1sgmM,69
|
|
184
|
-
canvas_sdk/templates/utils.py,sha256=
|
|
188
|
+
canvas_sdk/templates/utils.py,sha256=h-BAX4rm-EpqKMnFGMZF1RbXaWsOr6-ECQYhmJ8skyI,1422
|
|
185
189
|
canvas_sdk/templates/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
186
190
|
canvas_sdk/templates/tests/test_utils.py,sha256=VRahmmVwXKcp1NMLoA3BZL4cFFXzFnD-i5IUpcEeXTg,1832
|
|
187
191
|
canvas_sdk/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
188
192
|
canvas_sdk/utils/__init__.py,sha256=nZEfYeU-qNZBOh39b8zAuEh0Wzh2PgXxs0NRKKe66Pg,184
|
|
189
|
-
canvas_sdk/utils/db.py,sha256=0AO5bhu-k9OsAHpXe1RHzyDZnBGHUEzrv8-vYtTIoeA,592
|
|
190
193
|
canvas_sdk/utils/http.py,sha256=McFtcrgdR2W8XguTaTF54O5Xf5r5buoYDldDM5H5ahY,5846
|
|
194
|
+
canvas_sdk/utils/plugins.py,sha256=853MW2fiLpyG3o9ISEawAthQeRiZP73cai5Tngwu4MY,767
|
|
191
195
|
canvas_sdk/utils/stats.py,sha256=sJhIW_IssUVefQN6rrUAt1P0KvVIUIYcnpZlMHLibNA,732
|
|
192
196
|
canvas_sdk/utils/tests.py,sha256=0Buh_7PvDU1D081_rSJoYSJwIHMOBbL0gtGS3bSKe7s,2285
|
|
193
197
|
canvas_sdk/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
194
198
|
canvas_sdk/v1/apps.py,sha256=z5HVdICPLYrbWM8ZXK89Xu7RWaXuKU4AFRrPAZGz0C4,151
|
|
195
199
|
canvas_sdk/v1/models.py,sha256=q9Sofiu9JDH5g04H8NHYIqAtBYxH4KjnwOldoKsY9Lk,206
|
|
196
|
-
canvas_sdk/v1/data/__init__.py,sha256=
|
|
200
|
+
canvas_sdk/v1/data/__init__.py,sha256=SAAiRSaYly47oB8uyfjf6e4chTULX7M6qisR-fk2eHQ,3052
|
|
197
201
|
canvas_sdk/v1/data/allergy_intolerance.py,sha256=cm29fCOFiHHHVyff9kreWFUPZM--Nydw3a0FSbWh5-w,2303
|
|
198
202
|
canvas_sdk/v1/data/appointment.py,sha256=rK4Vl2bE_aE73vGx8Elj-tJdDdIbQNTbl0uLtWme89Y,1900
|
|
199
203
|
canvas_sdk/v1/data/assessment.py,sha256=MYUrE6xtOVYSRiMuQKMdHuWFvuGx62w4SZJpyFskQ2U,1485
|
|
200
204
|
canvas_sdk/v1/data/base.py,sha256=_UnCf0SP_HC9FYEzB1BsJ3QcKqXNdCR90Dtx1Gggcf4,6328
|
|
201
|
-
canvas_sdk/v1/data/billing.py,sha256=
|
|
205
|
+
canvas_sdk/v1/data/billing.py,sha256=dPQb9vj2mIhlU5cSfmfTkXw0NRDMmueJojyRNrgx1no,2586
|
|
202
206
|
canvas_sdk/v1/data/care_team.py,sha256=qy9x-FZ6LI_GrXnohS5izc6e-SANhV50eh_bilaVI4Y,1818
|
|
203
207
|
canvas_sdk/v1/data/command.py,sha256=r1hFQDFkGTAcpfRqCsQICcKivtSt08Mdz2jcKS8m9TA,1613
|
|
204
208
|
canvas_sdk/v1/data/common.py,sha256=JB8Lf8pHTo8VttZTnQFthnKLjalX8iI-ZoBsjpVMxHs,4559
|
|
205
209
|
canvas_sdk/v1/data/condition.py,sha256=olHJFb7A9ORWlMzolpt15fQmg47F--5oEM_lrAQGeMk,2096
|
|
206
|
-
canvas_sdk/v1/data/coverage.py,sha256=
|
|
210
|
+
canvas_sdk/v1/data/coverage.py,sha256=buz0VwJvyKJcX-EallQweA-5P0UxSjt6QLg3f8sMHEQ,10836
|
|
207
211
|
canvas_sdk/v1/data/detected_issue.py,sha256=hvzUIjLx2unA_GNdW1oJcsLRx8jIEOKHWP8j_p88s4k,1685
|
|
208
212
|
canvas_sdk/v1/data/device.py,sha256=RZ200OCaBRGwdERvx9eFOPTHg56r62Hqp9uJeS2giOo,1613
|
|
209
213
|
canvas_sdk/v1/data/imaging.py,sha256=2caHmSQmt1zUx5q_AoydkuUqtkQo-tdMk5B_0suSlWw,4297
|
|
@@ -216,6 +220,7 @@ canvas_sdk/v1/data/patient.py,sha256=tKsZRaexpanDtCHTy4HtEsodiw7BKk8hPcEwpF98o-g
|
|
|
216
220
|
canvas_sdk/v1/data/practicelocation.py,sha256=6W2NzwiK0xXlzXfDom_Wm3ScERd8W6mw8obSTFZWYLc,4206
|
|
217
221
|
canvas_sdk/v1/data/protocol_override.py,sha256=o-GK7-HkOsyYDLxUyn9BwlZM-13QNuGGNrey5EF44QI,2105
|
|
218
222
|
canvas_sdk/v1/data/questionnaire.py,sha256=1Vo5QUsOLYbZiQ64QFpgY7Mj6wY8vsV8f9uPiFl0wac,7031
|
|
223
|
+
canvas_sdk/v1/data/reason_for_visit.py,sha256=9rCmvbJZ4dKUUDzqFX1uxlMa_qO57KcRntV3re8q_8g,608
|
|
219
224
|
canvas_sdk/v1/data/staff.py,sha256=QyFxvFAvNwgTm4gf-uPH6A8ibFE2TBhEckZpAM7gNpw,2762
|
|
220
225
|
canvas_sdk/v1/data/task.py,sha256=5KNn88APPNOHEk4s1ZJRBBav8-AEQTUH039Vio_ZtAk,3471
|
|
221
226
|
canvas_sdk/v1/data/team.py,sha256=FlkZsHSSHKtvmlB1XuGPUmC7wl8iMbOPi_fl9sYw-aU,2801
|
|
@@ -249,13 +254,13 @@ plugin_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
249
254
|
plugin_runner/authentication.py,sha256=SDPso2AogtLAV_H0LuMDp99IMZuF3oTq-Q_AXAvJ8uc,1116
|
|
250
255
|
plugin_runner/aws_headers.py,sha256=DenX_nAMVhXMJZw88PLZbqJsi5_XriNtr3jE-eJqHY4,2773
|
|
251
256
|
plugin_runner/exceptions.py,sha256=YnRZiQVzbU3HrVlmEXLje_np99009YnhTRVHHyBCtqc,433
|
|
252
|
-
plugin_runner/plugin_installer.py,sha256=
|
|
257
|
+
plugin_runner/plugin_installer.py,sha256=8hWllx7YciatEozFRwcp5Y_L_KyEi2e8CGienRWrZs0,7375
|
|
253
258
|
plugin_runner/plugin_runner.py,sha256=h_FHStJrvRiG9DSf8uXHKFb4aby2wBzLAe10Ov4xnas,16437
|
|
254
|
-
plugin_runner/sandbox.py,sha256=
|
|
259
|
+
plugin_runner/sandbox.py,sha256=P64SKpKZdoJwk2NsirGRKq7xjygBtvdipIAVSD5roY8,14509
|
|
255
260
|
plugin_runner/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
256
261
|
plugin_runner/tests/test_application.py,sha256=e1R2YagMRD96gZALx-Zra-e-sR3SiP7cIpI6pheZnUc,2427
|
|
257
262
|
plugin_runner/tests/test_plugin_installer.py,sha256=i3ELG5cRfaG7ETLwk8aJhahgJuPfKjIrXL250t2wpzI,4434
|
|
258
|
-
plugin_runner/tests/test_plugin_runner.py,sha256=
|
|
263
|
+
plugin_runner/tests/test_plugin_runner.py,sha256=i1GAX8VsSU5KMwDOPYOQ5S9nxkRCWSjzG1nnh_IJLu4,11638
|
|
259
264
|
plugin_runner/tests/test_sandbox.py,sha256=fDIkRBzoVWClvIMOI37FvJ1TRZ5mAztt88YZ0OPqsA8,4231
|
|
260
265
|
plugin_runner/tests/data/plugins/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
261
266
|
plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json,sha256=J9T_E5vqUX4HITHbFsd6JQpw3YvMS4wR_lhI5JL2KMk,749
|
|
@@ -271,6 +276,11 @@ plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__in
|
|
|
271
276
|
plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py,sha256=apbdzjKtqeQJpPnAxgKituh0ZG71jK0SlwtQRSiV-4o,170
|
|
272
277
|
plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py,sha256=ZvCN37hADp2VG1-DfJE_dtHrXf-yirF0321f92WzC20,79
|
|
273
278
|
plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py,sha256=--erTS2NGGcw4aVhabrKTtHtuR5OHjc8rsK30fvuYZs,83
|
|
279
|
+
plugin_runner/tests/fixtures/plugins/test_load_questionnaire/CANVAS_MANIFEST.json,sha256=orbmUtS3uGl6XNiubCo79H4Jr_DbydMtzvdXUuk6cAM,1579
|
|
280
|
+
plugin_runner/tests/fixtures/plugins/test_load_questionnaire/README.md,sha256=D3SFO-pnyZjpEFLylWaIWi8AmUW8ZxMyHDddN4a7vAQ,246
|
|
281
|
+
plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
282
|
+
plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/my_protocol.py,sha256=_dWsMARIefDi8FlMhZIN0j62-0EjXimdfu42ZEhtDzU,1444
|
|
283
|
+
plugin_runner/tests/fixtures/plugins/test_load_questionnaire/questionnaires/example_questionnaire.yml,sha256=PjbCH4bbRFDmrciM-mJWjrKG7KTFhcSgZu1w6dAorio,1940
|
|
274
284
|
plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json,sha256=o84LSegKZ7eH_MeGkmRZFJJPEcOYHUzT0P2QkQpCZac,793
|
|
275
285
|
plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md,sha256=ZuFqRirQNoqdPzBjfpYzTGpIogmiQ_noX59hY0W2KHU,292
|
|
276
286
|
plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -318,7 +328,7 @@ protobufs/canvas_generated/messages/plugins.proto,sha256=oNainUPWFYQjgCX7bJEPI9_
|
|
|
318
328
|
protobufs/canvas_generated/services/plugin_runner.proto,sha256=doadBKn5k4xAtOgR-q_pEvW4yzxpUaHNOowMG6CL5GY,304
|
|
319
329
|
pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
320
330
|
pubsub/pubsub.py,sha256=pyTW0JU8mtaqiAV6g6xjZwel1CVy2EonPMU-_vkmhUM,1044
|
|
321
|
-
canvas-0.
|
|
322
|
-
canvas-0.
|
|
323
|
-
canvas-0.
|
|
324
|
-
canvas-0.
|
|
331
|
+
canvas-0.19.1.dist-info/METADATA,sha256=7w1TpwcKJ-BjC9HRzHYW4OW6LzUFr1gHFxlqRrIkX-U,4375
|
|
332
|
+
canvas-0.19.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
333
|
+
canvas-0.19.1.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
|
|
334
|
+
canvas-0.19.1.dist-info/RECORD,,
|
|
@@ -16,6 +16,7 @@ manifest_schema = {
|
|
|
16
16
|
"effects": {"$ref": "#/$defs/component"},
|
|
17
17
|
"views": {"$ref": "#/$defs/component"},
|
|
18
18
|
"applications": {"$ref": "#/$defs/applications"},
|
|
19
|
+
"questionnaires": {"$ref": "#/$defs/questionnaires"},
|
|
19
20
|
},
|
|
20
21
|
"additionalProperties": False,
|
|
21
22
|
"minProperties": 1,
|
|
@@ -101,5 +102,16 @@ manifest_schema = {
|
|
|
101
102
|
"additionalProperties": False,
|
|
102
103
|
},
|
|
103
104
|
},
|
|
105
|
+
"questionnaires": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"properties": {
|
|
110
|
+
"template": {"type": "string"},
|
|
111
|
+
},
|
|
112
|
+
"required": ["template"],
|
|
113
|
+
"additionalProperties": False,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
104
116
|
},
|
|
105
117
|
}
|
canvas_sdk/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
|
+
from uuid import UUID
|
|
2
3
|
|
|
3
4
|
from pydantic_core import InitErrorDetails
|
|
4
5
|
|
|
5
6
|
from canvas_sdk.commands.base import _BaseCommand
|
|
6
7
|
from canvas_sdk.commands.constants import Coding
|
|
8
|
+
from canvas_sdk.v1.data import ReasonForVisitSettingCoding
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class ReasonForVisitCommand(_BaseCommand):
|
|
@@ -13,8 +15,7 @@ class ReasonForVisitCommand(_BaseCommand):
|
|
|
13
15
|
key = "reasonForVisit"
|
|
14
16
|
|
|
15
17
|
structured: bool = False
|
|
16
|
-
|
|
17
|
-
coding: Coding | None = None
|
|
18
|
+
coding: Coding | UUID | str | None = None
|
|
18
19
|
comment: str | None = None
|
|
19
20
|
|
|
20
21
|
def _get_error_details(
|
|
@@ -27,6 +28,16 @@ class ReasonForVisitCommand(_BaseCommand):
|
|
|
27
28
|
"value", "Structured RFV should have a coding.", self.coding
|
|
28
29
|
)
|
|
29
30
|
)
|
|
31
|
+
|
|
32
|
+
if self.coding:
|
|
33
|
+
if isinstance(self.coding, str | UUID):
|
|
34
|
+
query = {"id": self.coding}
|
|
35
|
+
error_message = f"ReasonForVisitSettingCoding with id {self.coding} does not exist."
|
|
36
|
+
else:
|
|
37
|
+
query = {"code": self.coding["code"], "system": self.coding["system"]}
|
|
38
|
+
error_message = f"ReasonForVisitSettingCoding with code {self.coding['code']} and system {self.coding['system']} does not exist."
|
|
39
|
+
if not ReasonForVisitSettingCoding.objects.filter(**query).exists():
|
|
40
|
+
errors.append(self._create_error_detail("value", error_message, self.coding))
|
|
30
41
|
return errors
|
|
31
42
|
|
|
32
43
|
@classmethod
|
|
@@ -40,4 +51,8 @@ class ReasonForVisitCommand(_BaseCommand):
|
|
|
40
51
|
@property
|
|
41
52
|
def values(self) -> dict:
|
|
42
53
|
"""The ReasonForVisit command's field values."""
|
|
43
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
"structured": self.structured,
|
|
56
|
+
"coding": str(self.coding) if isinstance(self.coding, UUID) else self.coding,
|
|
57
|
+
"comment": self.comment,
|
|
58
|
+
}
|
|
@@ -37,6 +37,12 @@ from canvas_sdk.commands.constants import ClinicalQuantity, Coding
|
|
|
37
37
|
runner = CliRunner()
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class WrongType:
|
|
41
|
+
"""A type to yield ValidationErrors in tests."""
|
|
42
|
+
|
|
43
|
+
wrong_field: str
|
|
44
|
+
|
|
45
|
+
|
|
40
46
|
class MaskedValue:
|
|
41
47
|
"""A class to mask sensitive values in tests."""
|
|
42
48
|
|
|
@@ -97,6 +103,8 @@ def fake(field_props: dict, Command: type[_BaseCommand]) -> Any:
|
|
|
97
103
|
return Coding(system=random_string(), code=random_string(), display=random_string())
|
|
98
104
|
case "ClinicalQuantity":
|
|
99
105
|
return ClinicalQuantity(representative_ndc="ndc", ncpdp_quantity_qualifier_code="code")
|
|
106
|
+
case "WrongType":
|
|
107
|
+
return WrongType()
|
|
100
108
|
if t[0].isupper():
|
|
101
109
|
return random.choice(list(getattr(Command, t)))
|
|
102
110
|
|
|
@@ -108,7 +116,8 @@ def raises_wrong_type_error(
|
|
|
108
116
|
"""Test that the correct error is raised when the wrong type is passed to a field."""
|
|
109
117
|
field_props = Command.model_json_schema()["properties"][field]
|
|
110
118
|
field_type = get_field_type(field_props)
|
|
111
|
-
|
|
119
|
+
|
|
120
|
+
wrong_field_type = "WrongType"
|
|
112
121
|
|
|
113
122
|
with pytest.raises(ValidationError) as e1:
|
|
114
123
|
err_kwargs = {field: fake({"type": wrong_field_type}, Command)}
|
|
@@ -122,8 +131,11 @@ def raises_wrong_type_error(
|
|
|
122
131
|
setattr(cmd, field, err_value)
|
|
123
132
|
err_msg2 = repr(e2.value)
|
|
124
133
|
|
|
125
|
-
assert
|
|
126
|
-
assert f"
|
|
134
|
+
assert "validation error" in err_msg1
|
|
135
|
+
assert f"{Command.__name__}\n{field}" in err_msg1
|
|
136
|
+
|
|
137
|
+
assert "validation error" in err_msg2
|
|
138
|
+
assert f"{Command.__name__}\n{field}" in err_msg1
|
|
127
139
|
|
|
128
140
|
field_type = (
|
|
129
141
|
"dictionary" if field_type == "Coding" or field_type == "ClinicalQuantity" else field_type
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
from pydantic import ValidationError
|
|
3
5
|
from typer.testing import CliRunner
|
|
@@ -91,24 +93,21 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
@pytest.mark.parametrize(
|
|
94
|
-
"Command,err_kwargs,
|
|
96
|
+
"Command,err_kwargs,valid_kwargs",
|
|
95
97
|
[
|
|
96
98
|
(
|
|
97
99
|
PlanCommand,
|
|
98
100
|
{"narrative": "yo", "note_uuid": 1},
|
|
99
|
-
"1 validation error for PlanCommand\nnote_uuid\n Input should be a valid string [type=string_type",
|
|
100
101
|
{"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
101
102
|
),
|
|
102
103
|
(
|
|
103
104
|
PlanCommand,
|
|
104
105
|
{"narrative": "yo", "note_uuid": "5", "command_uuid": 5},
|
|
105
|
-
"1 validation error for PlanCommand\ncommand_uuid\n Input should be a valid string [type=string_type",
|
|
106
106
|
{"narrative": "yo", "note_uuid": "5", "command_uuid": "5"},
|
|
107
107
|
),
|
|
108
108
|
(
|
|
109
109
|
ReasonForVisitCommand,
|
|
110
110
|
{"note_uuid": "00000000-0000-0000-0000-000000000000", "structured": True},
|
|
111
|
-
"1 validation error for ReasonForVisitCommand\n Structured RFV should have a coding",
|
|
112
111
|
{
|
|
113
112
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
114
113
|
"structured": False,
|
|
@@ -120,7 +119,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
120
119
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
121
120
|
"coding": {"code": "x"},
|
|
122
121
|
},
|
|
123
|
-
"1 validation error for ReasonForVisitCommand\ncoding.system\n Field required [type=missing",
|
|
124
122
|
{"note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
125
123
|
),
|
|
126
124
|
(
|
|
@@ -129,7 +127,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
129
127
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
130
128
|
"coding": {"code": 1, "system": "y"},
|
|
131
129
|
},
|
|
132
|
-
"1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
|
|
133
130
|
{"note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
134
131
|
),
|
|
135
132
|
(
|
|
@@ -138,7 +135,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
138
135
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
139
136
|
"coding": {"code": None, "system": "y"},
|
|
140
137
|
},
|
|
141
|
-
"1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
|
|
142
138
|
{"note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
143
139
|
),
|
|
144
140
|
(
|
|
@@ -147,7 +143,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
147
143
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
148
144
|
"coding": {"system": "y"},
|
|
149
145
|
},
|
|
150
|
-
"1 validation error for ReasonForVisitCommand\ncoding.code\n Field required [type=missing",
|
|
151
146
|
{"note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
152
147
|
),
|
|
153
148
|
(
|
|
@@ -156,7 +151,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
156
151
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
157
152
|
"coding": {"code": "x", "system": 1},
|
|
158
153
|
},
|
|
159
|
-
"1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
|
|
160
154
|
{"note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
161
155
|
),
|
|
162
156
|
(
|
|
@@ -165,7 +159,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
165
159
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
166
160
|
"coding": {"code": "x", "system": None},
|
|
167
161
|
},
|
|
168
|
-
"1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
|
|
169
162
|
{"note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
170
163
|
),
|
|
171
164
|
(
|
|
@@ -174,7 +167,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
174
167
|
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
175
168
|
"coding": {"code": "x", "system": "y", "display": 1},
|
|
176
169
|
},
|
|
177
|
-
"1 validation error for ReasonForVisitCommand\ncoding.display\n Input should be a valid string [type=string_type",
|
|
178
170
|
{"note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
179
171
|
),
|
|
180
172
|
],
|
|
@@ -182,25 +174,24 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
182
174
|
def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
|
|
183
175
|
Command: type[PlanCommand] | type[ReasonForVisitCommand],
|
|
184
176
|
err_kwargs: dict,
|
|
185
|
-
err_msg: str,
|
|
186
177
|
valid_kwargs: dict,
|
|
187
178
|
) -> None:
|
|
188
179
|
"""Test that Command raises a specific error when a kwarg is given an incorrect type."""
|
|
189
|
-
with pytest.raises(ValidationError)
|
|
180
|
+
with pytest.raises(ValidationError):
|
|
190
181
|
cmd = Command(**err_kwargs)
|
|
191
182
|
cmd.originate()
|
|
183
|
+
cmd.command_uuid = str(uuid.uuid4())
|
|
192
184
|
cmd.edit()
|
|
193
|
-
assert err_msg in repr(e1.value)
|
|
194
185
|
|
|
195
186
|
cmd = Command(**valid_kwargs)
|
|
196
187
|
if len(err_kwargs) < len(valid_kwargs):
|
|
197
188
|
return
|
|
198
189
|
key, value = list(err_kwargs.items())[-1]
|
|
199
|
-
with pytest.raises(ValidationError)
|
|
190
|
+
with pytest.raises(ValidationError):
|
|
200
191
|
setattr(cmd, key, value)
|
|
201
192
|
cmd.originate()
|
|
193
|
+
cmd.command_uuid = str(uuid.uuid4())
|
|
202
194
|
cmd.edit()
|
|
203
|
-
assert err_msg in repr(e2.value)
|
|
204
195
|
|
|
205
196
|
|
|
206
197
|
@pytest.mark.parametrize(
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from canvas_sdk.effects import Effect
|
|
8
|
+
from canvas_sdk.events import Event, EventRequest, EventType
|
|
9
|
+
from canvas_sdk.questionnaires import questionnaire_from_yaml
|
|
10
|
+
from plugin_runner.plugin_runner import LOADED_PLUGINS
|
|
11
|
+
from settings import PLUGIN_DIRECTORY
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
|
|
15
|
+
def test_from_yaml_valid_questionnaire(install_test_plugin: Path, load_test_plugins: None) -> None:
|
|
16
|
+
"""Test that the from_yaml function loads a valid questionnaire."""
|
|
17
|
+
plugin = LOADED_PLUGINS[
|
|
18
|
+
"test_load_questionnaire:test_load_questionnaire.protocols.my_protocol:ValidQuestionnaire"
|
|
19
|
+
]
|
|
20
|
+
result: list[Effect] = plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
21
|
+
|
|
22
|
+
assert (
|
|
23
|
+
yaml.load(
|
|
24
|
+
(
|
|
25
|
+
Path(PLUGIN_DIRECTORY)
|
|
26
|
+
/ "test_load_questionnaire/questionnaires/example_questionnaire.yml"
|
|
27
|
+
)
|
|
28
|
+
.resolve()
|
|
29
|
+
.read_text(),
|
|
30
|
+
Loader=yaml.SafeLoader,
|
|
31
|
+
).items()
|
|
32
|
+
<= json.loads(result[0].payload).items()
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
|
|
37
|
+
def test_from_yaml_invalid_questionnaire(
|
|
38
|
+
install_test_plugin: Path, load_test_plugins: None
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Test that the from_yaml function raises an error for invalid questionnaires."""
|
|
41
|
+
plugin = LOADED_PLUGINS[
|
|
42
|
+
"test_load_questionnaire:test_load_questionnaire.protocols.my_protocol:InvalidQuestionnaire"
|
|
43
|
+
]
|
|
44
|
+
with pytest.raises(FileNotFoundError):
|
|
45
|
+
plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
|
|
49
|
+
def test_from_yaml_forbidden_questionnaire(
|
|
50
|
+
install_test_plugin: Path, load_test_plugins: None
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Test that the from_yaml function raises an error for a questionnaire outside plugin package."""
|
|
53
|
+
plugin = LOADED_PLUGINS[
|
|
54
|
+
"test_load_questionnaire:test_load_questionnaire.protocols.my_protocol:ForbiddenQuestionnaire"
|
|
55
|
+
]
|
|
56
|
+
with pytest.raises(PermissionError):
|
|
57
|
+
plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_from_yaml_non_plugin_caller() -> None:
|
|
61
|
+
"""Test that the from_yaml function returns None when called outside a plugin."""
|
|
62
|
+
assert questionnaire_from_yaml("questionnaires/example_questionnaire.yml") is None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
|
|
66
|
+
def test_from_yaml_sets_default_values(install_test_plugin: Path) -> None:
|
|
67
|
+
"""Test that the from_yaml function sets default values for properties."""
|
|
68
|
+
globals()["__is_plugin__"] = True
|
|
69
|
+
globals()["__name__"] = "test_load_questionnaire"
|
|
70
|
+
|
|
71
|
+
definition = questionnaire_from_yaml("questionnaires/example_questionnaire.yml")
|
|
72
|
+
|
|
73
|
+
assert definition is not None
|
|
74
|
+
assert definition["display_results_in_social_history_section"] is False
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import json
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, TypedDict
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from jsonschema import Draft7Validator, validators
|
|
9
|
+
|
|
10
|
+
from canvas_sdk.utils.plugins import plugin_only
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Response(TypedDict):
|
|
14
|
+
"""A Response of a Questionnaire."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
code: str
|
|
18
|
+
code_description: str
|
|
19
|
+
value: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Question(TypedDict):
|
|
23
|
+
"""A Question of a Questionnaire."""
|
|
24
|
+
|
|
25
|
+
code_system: str
|
|
26
|
+
code: str
|
|
27
|
+
code_description: str
|
|
28
|
+
content: str
|
|
29
|
+
responses_code_system: str
|
|
30
|
+
responses_type: str
|
|
31
|
+
display_result_in_social_history_section: bool
|
|
32
|
+
responses: list[Response]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class QuestionnaireConfig(TypedDict):
|
|
36
|
+
"""A Questionnaire configuration."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
form_type: str
|
|
40
|
+
code_system: str
|
|
41
|
+
code: str
|
|
42
|
+
can_originate_in_charting: bool
|
|
43
|
+
prologue: str
|
|
44
|
+
display_results_in_social_history_section: bool
|
|
45
|
+
questions: list[Question]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extend_with_defaults(validator_class: type[Draft7Validator]) -> type[Draft7Validator]:
|
|
49
|
+
"""Extend a Draft7Validator with default values for properties."""
|
|
50
|
+
validate_properties = validator_class.VALIDATORS["properties"]
|
|
51
|
+
|
|
52
|
+
def set_defaults(
|
|
53
|
+
validator: Draft7Validator,
|
|
54
|
+
properties: dict[str, Any],
|
|
55
|
+
instance: dict[str, Any],
|
|
56
|
+
schema: dict[str, Any],
|
|
57
|
+
) -> Generator[Any, None, None]:
|
|
58
|
+
for property, subschema in properties.items():
|
|
59
|
+
if "default" in subschema:
|
|
60
|
+
instance.setdefault(property, subschema["default"])
|
|
61
|
+
|
|
62
|
+
yield from validate_properties(
|
|
63
|
+
validator,
|
|
64
|
+
properties,
|
|
65
|
+
instance,
|
|
66
|
+
schema,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return validators.extend(
|
|
70
|
+
validator_class,
|
|
71
|
+
{"properties": set_defaults},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
ExtendedDraft7Validator = extend_with_defaults(Draft7Validator)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@plugin_only
|
|
79
|
+
def from_yaml(questionnaire_name: str, **kwargs: Any) -> QuestionnaireConfig | None:
|
|
80
|
+
"""Load a Questionnaire configuration from a YAML file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
questionnaire_name (str): The path to the questionnaire file, relative to the plugin package.
|
|
84
|
+
If the path starts with a forward slash ("/"), it will be stripped during resolution.
|
|
85
|
+
kwargs (Any): Additional keyword arguments.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
QuestionnaireConfig: The loaded Questionnaire configuration.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
FileNotFoundError: If the questionnaire file does not exist within the plugin's directory
|
|
92
|
+
or if the resolved path is invalid.
|
|
93
|
+
PermissionError: If the resolved path is outside the plugin's directory.
|
|
94
|
+
ValidationError: If the questionnaire file does not conform to the JSON schema.
|
|
95
|
+
"""
|
|
96
|
+
plugin_dir = kwargs["plugin_dir"]
|
|
97
|
+
questionnaire_config_path = Path(plugin_dir / questionnaire_name.lstrip("/")).resolve()
|
|
98
|
+
|
|
99
|
+
if not questionnaire_config_path.is_relative_to(plugin_dir):
|
|
100
|
+
raise PermissionError(f"Invalid Questionnaire '{questionnaire_name}'")
|
|
101
|
+
elif not questionnaire_config_path.exists():
|
|
102
|
+
raise FileNotFoundError(f"Questionnaire {questionnaire_name} not found.")
|
|
103
|
+
|
|
104
|
+
questionnaire_config = yaml.load(questionnaire_config_path.read_text(), Loader=yaml.SafeLoader)
|
|
105
|
+
ExtendedDraft7Validator(json_schema()).validate(questionnaire_config)
|
|
106
|
+
|
|
107
|
+
return questionnaire_config
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@functools.cache
|
|
111
|
+
def json_schema() -> dict[str, Any]:
|
|
112
|
+
"""Reads the JSON schema for a Questionnaire Config."""
|
|
113
|
+
schema = json.loads(
|
|
114
|
+
(Path(__file__).resolve().parent.parent.parent / "schemas/questionnaire.json").read_text()
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return schema
|
canvas_sdk/templates/utils.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import inspect
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
from typing import Any
|
|
4
3
|
|
|
5
4
|
from django.template import Context, Template
|
|
6
5
|
|
|
7
|
-
from
|
|
6
|
+
from canvas_sdk.utils.plugins import plugin_only
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
@plugin_only
|
|
10
|
+
def render_to_string(
|
|
11
|
+
template_name: str, context: dict[str, Any] | None = None, **kwargs: Any
|
|
12
|
+
) -> str | None:
|
|
11
13
|
"""Load a template and render it with the given context.
|
|
12
14
|
|
|
13
15
|
Args:
|
|
@@ -15,6 +17,7 @@ def render_to_string(template_name: str, context: dict[str, Any] | None = None)
|
|
|
15
17
|
If the path starts with a forward slash ("/"), it will be stripped during resolution.
|
|
16
18
|
context (dict[str, Any] | None): A dictionary of variables to pass to the template
|
|
17
19
|
for rendering. Defaults to None, which uses an empty context.
|
|
20
|
+
kwargs (Any): Additional keyword arguments.
|
|
18
21
|
|
|
19
22
|
Returns:
|
|
20
23
|
str: The rendered template as a string.
|
|
@@ -23,15 +26,7 @@ def render_to_string(template_name: str, context: dict[str, Any] | None = None)
|
|
|
23
26
|
FileNotFoundError: If the template file does not exist within the plugin's directory
|
|
24
27
|
or if the resolved path is invalid.
|
|
25
28
|
"""
|
|
26
|
-
|
|
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
|
|
29
|
+
plugin_dir = kwargs["plugin_dir"]
|
|
35
30
|
template_path = Path(plugin_dir / template_name.lstrip("/")).resolve()
|
|
36
31
|
|
|
37
32
|
if not template_path.is_relative_to(plugin_dir):
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from settings import PLUGIN_DIRECTORY
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
10
|
+
"""Decorator to restrict a function's execution to plugins only."""
|
|
11
|
+
|
|
12
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
13
|
+
current_frame = inspect.currentframe()
|
|
14
|
+
caller = current_frame.f_back if current_frame else None
|
|
15
|
+
|
|
16
|
+
if not caller or "__is_plugin__" not in caller.f_globals:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
plugin_name = caller.f_globals["__name__"].split(".")[0]
|
|
20
|
+
plugin_dir = Path(PLUGIN_DIRECTORY) / plugin_name
|
|
21
|
+
kwargs["plugin_dir"] = plugin_dir.resolve()
|
|
22
|
+
|
|
23
|
+
return func(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
return wrapper
|
canvas_sdk/v1/data/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .allergy_intolerance import AllergyIntolerance, AllergyIntoleranceCoding
|
|
2
2
|
from .appointment import Appointment
|
|
3
3
|
from .assessment import Assessment
|
|
4
|
-
from .billing import BillingLineItem
|
|
4
|
+
from .billing import BillingLineItem, BillingLineItemModifier
|
|
5
5
|
from .care_team import CareTeamMembership, CareTeamRole
|
|
6
6
|
from .command import Command
|
|
7
7
|
from .condition import Condition, ConditionCoding
|
|
@@ -48,6 +48,7 @@ from .questionnaire import (
|
|
|
48
48
|
ResponseOption,
|
|
49
49
|
ResponseOptionSet,
|
|
50
50
|
)
|
|
51
|
+
from .reason_for_visit import ReasonForVisitSettingCoding
|
|
51
52
|
from .staff import Staff
|
|
52
53
|
from .task import Task, TaskComment, TaskLabel, TaskTaskLabel
|
|
53
54
|
from .user import CanvasUser
|
|
@@ -58,6 +59,7 @@ __all__ = [
|
|
|
58
59
|
"AllergyIntoleranceCoding",
|
|
59
60
|
"Assessment",
|
|
60
61
|
"BillingLineItem",
|
|
62
|
+
"BillingLineItemModifier",
|
|
61
63
|
"CanvasUser",
|
|
62
64
|
"CareTeamMembership",
|
|
63
65
|
"CareTeamRole",
|
|
@@ -103,6 +105,7 @@ __all__ = [
|
|
|
103
105
|
"Question",
|
|
104
106
|
"Questionnaire",
|
|
105
107
|
"QuestionnaireQuestionMap",
|
|
108
|
+
"ReasonForVisitSettingCoding",
|
|
106
109
|
"ResponseOption",
|
|
107
110
|
"ResponseOptionSet",
|
|
108
111
|
"Staff",
|
canvas_sdk/v1/data/billing.py
CHANGED
|
@@ -44,10 +44,16 @@ class BillingLineItem(models.Model):
|
|
|
44
44
|
created = models.DateTimeField()
|
|
45
45
|
modified = models.DateTimeField()
|
|
46
46
|
note = models.ForeignKey(
|
|
47
|
-
"v1.Note",
|
|
47
|
+
"v1.Note",
|
|
48
|
+
on_delete=models.DO_NOTHING,
|
|
49
|
+
related_name="billing_line_items",
|
|
50
|
+
null=True,
|
|
48
51
|
)
|
|
49
52
|
patient = models.ForeignKey(
|
|
50
|
-
"v1.Patient",
|
|
53
|
+
"v1.Patient",
|
|
54
|
+
on_delete=models.DO_NOTHING,
|
|
55
|
+
related_name="billing_line_items",
|
|
56
|
+
null=True,
|
|
51
57
|
)
|
|
52
58
|
cpt = models.CharField()
|
|
53
59
|
charge = models.DecimalField()
|
|
@@ -56,3 +62,24 @@ class BillingLineItem(models.Model):
|
|
|
56
62
|
command_type = models.CharField()
|
|
57
63
|
command_id = models.IntegerField()
|
|
58
64
|
status = models.CharField(choices=BillingLineItemStatus.choices)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BillingLineItemModifier(models.Model):
|
|
68
|
+
"""BillingLineItemModifier."""
|
|
69
|
+
|
|
70
|
+
class Meta:
|
|
71
|
+
managed = False
|
|
72
|
+
db_table = "canvas_sdk_data_api_billinglineitemmodifier_001"
|
|
73
|
+
|
|
74
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
75
|
+
system = models.CharField()
|
|
76
|
+
version = models.CharField()
|
|
77
|
+
code = models.CharField()
|
|
78
|
+
display = models.CharField()
|
|
79
|
+
user_selected = models.BooleanField()
|
|
80
|
+
line_item = models.ForeignKey(
|
|
81
|
+
"v1.BillingLineItem",
|
|
82
|
+
on_delete=models.DO_NOTHING,
|
|
83
|
+
related_name="modifiers",
|
|
84
|
+
null=True,
|
|
85
|
+
)
|
canvas_sdk/v1/data/coverage.py
CHANGED
|
@@ -46,7 +46,7 @@ class CoverageRelationshipCode(models.TextChoices):
|
|
|
46
46
|
"""CoverageRelationshipCode."""
|
|
47
47
|
|
|
48
48
|
SELF = "18", "Self"
|
|
49
|
-
SPOUSE = "01" "Spouse"
|
|
49
|
+
SPOUSE = "01", "Spouse"
|
|
50
50
|
CHILD_INSURED_HAS_FINANCIAL_RESP = "19", "Natural Child, insured has financial responsibility"
|
|
51
51
|
CHILD_HAS_FINANCIAL_RESP = "43", "Natural Child, insured does not have financial responsibility"
|
|
52
52
|
STEP_CHILD = "17", "Step Child"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django.contrib.postgres.fields import ArrayField
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ReasonForVisitSettingCoding(models.Model):
|
|
6
|
+
"""ReasonForVisitSettingCoding."""
|
|
7
|
+
|
|
8
|
+
class Meta:
|
|
9
|
+
managed = False
|
|
10
|
+
db_table = "canvas_sdk_data_api_reasonforvisitsettingcoding_001"
|
|
11
|
+
|
|
12
|
+
objects: models.Manager["ReasonForVisitSettingCoding"]
|
|
13
|
+
|
|
14
|
+
id = models.UUIDField()
|
|
15
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
16
|
+
|
|
17
|
+
code = models.CharField()
|
|
18
|
+
display = models.CharField()
|
|
19
|
+
system = models.CharField()
|
|
20
|
+
version = models.CharField()
|
|
21
|
+
|
|
22
|
+
duration = ArrayField(models.DurationField())
|
|
@@ -6,7 +6,7 @@ import tempfile
|
|
|
6
6
|
from collections.abc import Generator
|
|
7
7
|
from contextlib import contextmanager
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import TypedDict, cast
|
|
10
10
|
from urllib import parse
|
|
11
11
|
|
|
12
12
|
import psycopg
|
|
@@ -30,41 +30,32 @@ from settings import (
|
|
|
30
30
|
UPLOAD_TO_PREFIX = "plugins"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
"""
|
|
35
|
-
parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
|
|
36
|
-
db_name = parsed_url.path[1:]
|
|
37
|
-
return {
|
|
38
|
-
"dbname": db_name,
|
|
39
|
-
"user": parsed_url.username,
|
|
40
|
-
"password": parsed_url.password,
|
|
41
|
-
"host": parsed_url.hostname,
|
|
42
|
-
"port": parsed_url.port,
|
|
43
|
-
}
|
|
33
|
+
def open_database_connection() -> Connection:
|
|
34
|
+
"""Opens a psycopg connection to the home-app database.
|
|
44
35
|
|
|
36
|
+
When running within Aptible, use the database URL, otherwise pull from
|
|
37
|
+
the environment variables.
|
|
38
|
+
"""
|
|
39
|
+
if os.getenv("DATABASE_URL"):
|
|
40
|
+
parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
|
|
41
|
+
|
|
42
|
+
return psycopg.connect(
|
|
43
|
+
dbname=cast(str, parsed_url.path[1:]),
|
|
44
|
+
user=cast(str, parsed_url.username),
|
|
45
|
+
password=cast(str, parsed_url.password),
|
|
46
|
+
host=cast(str, parsed_url.hostname),
|
|
47
|
+
port=parsed_url.port,
|
|
48
|
+
)
|
|
45
49
|
|
|
46
|
-
def get_database_dict_from_env() -> dict[str, Any]:
|
|
47
|
-
"""Creates a psycopg ready dictionary from the environment variables."""
|
|
48
50
|
APP_NAME = os.getenv("APP_NAME")
|
|
49
51
|
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def open_database_connection() -> Connection:
|
|
60
|
-
"""Opens a psycopg connection to the home-app database."""
|
|
61
|
-
# When running within Aptible, use the database URL, otherwise pull from the environment variables.
|
|
62
|
-
if os.getenv("DATABASE_URL"):
|
|
63
|
-
database_dict = get_database_dict_from_url()
|
|
64
|
-
else:
|
|
65
|
-
database_dict = get_database_dict_from_env()
|
|
66
|
-
conn = psycopg.connect(**database_dict)
|
|
67
|
-
return conn
|
|
52
|
+
return psycopg.connect(
|
|
53
|
+
dbname=APP_NAME,
|
|
54
|
+
user=os.getenv("DB_USERNAME", "app"),
|
|
55
|
+
password=os.getenv("DB_PASSWORD", "app"),
|
|
56
|
+
host=os.getenv("DB_HOST", f"{APP_NAME}-db"),
|
|
57
|
+
port=os.getenv("DB_PORT", "5432"),
|
|
58
|
+
)
|
|
68
59
|
|
|
69
60
|
|
|
70
61
|
class PluginAttributes(TypedDict):
|
plugin_runner/sandbox.py
CHANGED
|
@@ -45,6 +45,7 @@ ALLOWED_MODULES = frozenset(
|
|
|
45
45
|
"canvas_sdk.events",
|
|
46
46
|
"canvas_sdk.handlers",
|
|
47
47
|
"canvas_sdk.protocols",
|
|
48
|
+
"canvas_sdk.questionnaires",
|
|
48
49
|
"canvas_sdk.utils",
|
|
49
50
|
"canvas_sdk.templates",
|
|
50
51
|
"canvas_sdk.v1",
|
|
@@ -173,12 +174,12 @@ class Sandbox:
|
|
|
173
174
|
):
|
|
174
175
|
self.warn(
|
|
175
176
|
node,
|
|
176
|
-
f'"{name}" is an invalid variable name because it
|
|
177
|
+
f'"{name}" is an invalid variable name because it starts with "_"',
|
|
177
178
|
)
|
|
178
179
|
elif name.endswith("__roles__"):
|
|
179
180
|
self.error(
|
|
180
181
|
node,
|
|
181
|
-
f'"{name}" is an invalid variable name because
|
|
182
|
+
f'"{name}" is an invalid variable name because it ends with "__roles__".',
|
|
182
183
|
)
|
|
183
184
|
elif name in FORBIDDEN_FUNC_NAMES:
|
|
184
185
|
self.error(node, f'"{name}" is a reserved name.')
|
|
@@ -215,21 +216,20 @@ class Sandbox:
|
|
|
215
216
|
if node.attr.startswith("_") and node.attr != "_":
|
|
216
217
|
self.warn(
|
|
217
218
|
node,
|
|
218
|
-
f'"{node.attr}" is an invalid attribute name because it starts
|
|
219
|
+
f'"{node.attr}" is an invalid attribute name because it starts with "_".',
|
|
219
220
|
)
|
|
220
221
|
|
|
221
222
|
if node.attr.endswith("__roles__"):
|
|
222
223
|
self.error(
|
|
223
224
|
node,
|
|
224
|
-
f'"{node.attr}" is an invalid attribute name because it ends '
|
|
225
|
-
'with "__roles__".',
|
|
225
|
+
f'"{node.attr}" is an invalid attribute name because it ends with "__roles__".',
|
|
226
226
|
)
|
|
227
227
|
|
|
228
228
|
if isinstance(node.ctx, ast.Load):
|
|
229
229
|
node = self.node_contents_visit(node)
|
|
230
230
|
new_node = ast.Call(
|
|
231
231
|
func=ast.Name("_getattr_", ast.Load()),
|
|
232
|
-
args=[node.value, ast.
|
|
232
|
+
args=[node.value, ast.Constant(node.attr)],
|
|
233
233
|
keywords=[],
|
|
234
234
|
)
|
|
235
235
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdk_version": "0.1.4",
|
|
3
|
+
"plugin_version": "0.0.1",
|
|
4
|
+
"name": "test_load_questionnaire",
|
|
5
|
+
"description": "Edit the description in CANVAS_MANIFEST.json",
|
|
6
|
+
"components": {
|
|
7
|
+
"protocols": [
|
|
8
|
+
{
|
|
9
|
+
"class": "test_load_questionnaire.protocols.my_protocol:ValidQuestionnaire",
|
|
10
|
+
"description": "A protocol that does xyz...",
|
|
11
|
+
"data_access": {
|
|
12
|
+
"event": "",
|
|
13
|
+
"read": [],
|
|
14
|
+
"write": []
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"class": "test_load_questionnaire.protocols.my_protocol:InvalidQuestionnaire",
|
|
19
|
+
"description": "A protocol that does xyz...",
|
|
20
|
+
"data_access": {
|
|
21
|
+
"event": "",
|
|
22
|
+
"read": [],
|
|
23
|
+
"write": []
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"class": "test_load_questionnaire.protocols.my_protocol:ForbiddenQuestionnaire",
|
|
28
|
+
"description": "A protocol that does xyz...",
|
|
29
|
+
"data_access": {
|
|
30
|
+
"event": "",
|
|
31
|
+
"read": [],
|
|
32
|
+
"write": []
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"commands": [],
|
|
37
|
+
"content": [],
|
|
38
|
+
"effects": [],
|
|
39
|
+
"views": [],
|
|
40
|
+
"questionnaires": [
|
|
41
|
+
{
|
|
42
|
+
"template": "questionnaires/example_questionnaire.yml"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"secrets": [],
|
|
47
|
+
"tags": {},
|
|
48
|
+
"references": [],
|
|
49
|
+
"license": "",
|
|
50
|
+
"diagram": false,
|
|
51
|
+
"readme": "./README.md"
|
|
52
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
test_load_questionnaire
|
|
2
|
+
====================
|
|
3
|
+
|
|
4
|
+
## Description
|
|
5
|
+
|
|
6
|
+
A description of this plugin
|
|
7
|
+
|
|
8
|
+
### Important Note!
|
|
9
|
+
|
|
10
|
+
The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
|
|
11
|
+
gets updated if you add, remove, or rename protocols.
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.effects import Effect
|
|
4
|
+
from canvas_sdk.events import EventType
|
|
5
|
+
from canvas_sdk.handlers import BaseHandler
|
|
6
|
+
from canvas_sdk.questionnaires import questionnaire_from_yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ValidQuestionnaire(BaseHandler):
|
|
10
|
+
"""You should put a helpful description of this protocol's behavior here."""
|
|
11
|
+
|
|
12
|
+
RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
|
|
13
|
+
|
|
14
|
+
def compute(self) -> list[Effect]:
|
|
15
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
16
|
+
config = questionnaire_from_yaml("questionnaires/example_questionnaire.yml")
|
|
17
|
+
return [Effect(payload=json.dumps(config))]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InvalidQuestionnaire(BaseHandler):
|
|
21
|
+
"""You should put a helpful description of this protocol's behavior here."""
|
|
22
|
+
|
|
23
|
+
RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
|
|
24
|
+
|
|
25
|
+
def compute(self) -> list[Effect]:
|
|
26
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
27
|
+
questionnaire_from_yaml("questionnaires/example_questionnaire1.yml")
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ForbiddenQuestionnaire(BaseHandler):
|
|
32
|
+
"""You should put a helpful description of this protocol's behavior here."""
|
|
33
|
+
|
|
34
|
+
RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
|
|
35
|
+
|
|
36
|
+
def compute(self) -> list[Effect]:
|
|
37
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
38
|
+
questionnaire_from_yaml("../../questionnaires/example_questionnaire.yml")
|
|
39
|
+
return []
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../../../../../../schemas/questionnaire.json
|
|
2
|
+
|
|
3
|
+
name: Example Name
|
|
4
|
+
form_type: QUES
|
|
5
|
+
code_system: LOINC
|
|
6
|
+
code: QUES_EXAMPLE_NAME
|
|
7
|
+
can_originate_in_charting: true
|
|
8
|
+
prologue: This is an example of a structured assessment with single select, multiselect, and free text responses.
|
|
9
|
+
questions:
|
|
10
|
+
- content: "This is question #1"
|
|
11
|
+
code_system: CPT
|
|
12
|
+
code: H0005
|
|
13
|
+
code_description: ""
|
|
14
|
+
responses_code_system: INTERNAL
|
|
15
|
+
responses_type: SING
|
|
16
|
+
display_result_in_social_history_section: true
|
|
17
|
+
responses:
|
|
18
|
+
- name: "Single select response #1"
|
|
19
|
+
code: QUES_EXAMPLE_NAME_Q1_A1
|
|
20
|
+
code_description: ''
|
|
21
|
+
value: "1"
|
|
22
|
+
- name: "Single select response #2"
|
|
23
|
+
code: QUES_EXAMPLE_NAME_Q1_A2
|
|
24
|
+
code_description: ''
|
|
25
|
+
value: "0"
|
|
26
|
+
- name: "Single select response #3"
|
|
27
|
+
code: QUES_EXAMPLE_NAME_Q1_A3
|
|
28
|
+
code_description: ''
|
|
29
|
+
value: "0"
|
|
30
|
+
- content: "This is question #2"
|
|
31
|
+
code_system: INTERNAL
|
|
32
|
+
code: QUES_EXAMPLE_NAME_Q2
|
|
33
|
+
code_description: ""
|
|
34
|
+
responses_code_system: ICD-10
|
|
35
|
+
responses_type: MULT
|
|
36
|
+
display_result_in_social_history_section: true
|
|
37
|
+
responses:
|
|
38
|
+
- name: "Multi select response #1"
|
|
39
|
+
code: F1910
|
|
40
|
+
code_description: ''
|
|
41
|
+
value: "0"
|
|
42
|
+
- name: "Multi select response #2"
|
|
43
|
+
code: QUES_EXAMPLE_NAME_Q1_A1
|
|
44
|
+
code_description: ''
|
|
45
|
+
value: "2"
|
|
46
|
+
- name: "Multi select response #3"
|
|
47
|
+
code: QUES_EXAMPLE_NAME_Q1_A2
|
|
48
|
+
code_description: ''
|
|
49
|
+
value: "0"
|
|
50
|
+
- content: "This is question #3"
|
|
51
|
+
code_system: INTERNAL
|
|
52
|
+
code: QUES_EXAMPLE_NAME_Q3
|
|
53
|
+
code_description: ""
|
|
54
|
+
responses_code_system: INTERNAL
|
|
55
|
+
responses_type: TXT
|
|
56
|
+
display_result_in_social_history_section: true
|
|
57
|
+
responses:
|
|
58
|
+
- name: "Free text response"
|
|
59
|
+
code: QUES_EXAMPLE_NAME_Q3_A1
|
|
60
|
+
code_description: ''
|
|
61
|
+
value: "This is a default pre-populated free text response."
|
|
@@ -83,9 +83,9 @@ def test_load_plugins_with_plugin_that_imports_other_modules_outside_plugin_pack
|
|
|
83
83
|
with caplog.at_level(logging.ERROR):
|
|
84
84
|
load_or_reload_plugin(install_test_plugin)
|
|
85
85
|
|
|
86
|
-
assert any(
|
|
87
|
-
"
|
|
88
|
-
)
|
|
86
|
+
assert any("Error importing module" in record.message for record in caplog.records), (
|
|
87
|
+
"log.error() was not called with the expected message."
|
|
88
|
+
)
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
@pytest.mark.parametrize(
|
|
@@ -102,9 +102,9 @@ def test_load_plugins_with_plugin_that_imports_forbidden_modules(
|
|
|
102
102
|
with caplog.at_level(logging.ERROR):
|
|
103
103
|
load_or_reload_plugin(install_test_plugin)
|
|
104
104
|
|
|
105
|
-
assert any(
|
|
106
|
-
"
|
|
107
|
-
)
|
|
105
|
+
assert any("Error importing module" in record.message for record in caplog.records), (
|
|
106
|
+
"log.error() was not called with the expected message."
|
|
107
|
+
)
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
@pytest.mark.parametrize(
|
|
@@ -144,9 +144,9 @@ def test_plugin_that_implicitly_imports_allowed_modules(
|
|
|
144
144
|
]["class"]
|
|
145
145
|
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
146
146
|
|
|
147
|
-
assert any(
|
|
148
|
-
"
|
|
149
|
-
)
|
|
147
|
+
assert any("Hello, World!" in record.message for record in caplog.records), (
|
|
148
|
+
"log.info() with Template.render() was not called."
|
|
149
|
+
)
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
@pytest.mark.parametrize(
|
|
@@ -170,9 +170,9 @@ def test_plugin_that_implicitly_imports_forbidden_modules(
|
|
|
170
170
|
]["class"]
|
|
171
171
|
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
172
172
|
|
|
173
|
-
assert (
|
|
174
|
-
|
|
175
|
-
)
|
|
173
|
+
assert any("os list dir" in record.message for record in caplog.records) is False, (
|
|
174
|
+
"log.info() with os.listdir() was called."
|
|
175
|
+
)
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
settings.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
+
from urllib import parse
|
|
3
4
|
|
|
4
5
|
from dotenv import load_dotenv
|
|
5
6
|
from env_tools import env_to_bool
|
|
6
7
|
|
|
7
|
-
from canvas_sdk.utils.db import get_database_dict_from_url
|
|
8
|
-
|
|
9
8
|
load_dotenv()
|
|
10
9
|
|
|
11
10
|
ENV = os.getenv("ENV", "development")
|
|
@@ -40,19 +39,30 @@ CANVAS_SDK_DB_HOST = os.getenv("CANVAS_SDK_DB_HOST", "home-app-db")
|
|
|
40
39
|
CANVAS_SDK_DB_PORT = os.getenv("CANVAS_SDK_DB_PORT", "5432")
|
|
41
40
|
|
|
42
41
|
if os.getenv("DATABASE_URL"):
|
|
43
|
-
|
|
42
|
+
parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
|
|
43
|
+
|
|
44
|
+
DATABASES = {
|
|
45
|
+
"default": {
|
|
46
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
47
|
+
"NAME": parsed_url.path[1:],
|
|
48
|
+
"USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
|
|
49
|
+
"PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
|
|
50
|
+
"HOST": parsed_url.hostname,
|
|
51
|
+
"PORT": parsed_url.port,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
44
54
|
else:
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
DATABASES = {
|
|
56
|
+
"default": {
|
|
57
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
58
|
+
"NAME": CANVAS_SDK_DB_NAME,
|
|
59
|
+
"USER": CANVAS_SDK_DB_USERNAME,
|
|
60
|
+
"PASSWORD": CANVAS_SDK_DB_PASSWORD,
|
|
61
|
+
"HOST": CANVAS_SDK_DB_HOST,
|
|
62
|
+
"PORT": CANVAS_SDK_DB_PORT,
|
|
63
|
+
}
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
DATABASES = {"default": database_dict}
|
|
55
|
-
|
|
56
66
|
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "")
|
|
57
67
|
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "")
|
|
58
68
|
AWS_REGION = os.getenv("AWS_REGION", "us-west-2")
|
canvas_sdk/utils/db.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Any
|
|
3
|
-
from urllib import parse
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def get_database_dict_from_url() -> dict[str, Any]:
|
|
7
|
-
"""Retrieves the database URL for the data module connection formatted for Django settings."""
|
|
8
|
-
parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
|
|
9
|
-
db_name = parsed_url.path[1:]
|
|
10
|
-
return {
|
|
11
|
-
"ENGINE": "django.db.backends.postgresql",
|
|
12
|
-
"NAME": db_name,
|
|
13
|
-
"USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
|
|
14
|
-
"PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
|
|
15
|
-
"HOST": parsed_url.hostname,
|
|
16
|
-
"PORT": parsed_url.port,
|
|
17
|
-
}
|
|
File without changes
|
|
File without changes
|