Coverage for cc_modules/tests/cc_fhir_tests.py: 17%

424 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3"""camcops_server/cc_modules/tests/cc_fhir_tests.py 

4 

5=============================================================================== 

6 

7 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

8 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

9 

10 This file is part of CamCOPS. 

11 

12 CamCOPS is free software: you can redistribute it and/or modify 

13 it under the terms of the GNU General Public License as published by 

14 the Free Software Foundation, either version 3 of the License, or 

15 (at your option) any later version. 

16 

17 CamCOPS is distributed in the hope that it will be useful, 

18 but WITHOUT ANY WARRANTY; without even the implied warranty of 

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

22 You should have received a copy of the GNU General Public License 

23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

24 

25=============================================================================== 

26 

27""" 

28 

29import datetime 

30import json 

31import logging 

32from typing import Dict 

33from unittest import mock 

34 

35from cardinal_pythonlib.httpconst import HttpMethod 

36from cardinal_pythonlib.nhs import generate_random_nhs_number 

37import pendulum 

38from requests.exceptions import HTTPError 

39 

40from camcops_server.cc_modules.cc_constants import FHIRConst as Fc, JSON_INDENT 

41from camcops_server.cc_modules.cc_exportmodels import ( 

42 ExportedTask, 

43 ExportedTaskFhir, 

44) 

45from camcops_server.cc_modules.cc_exportrecipient import ExportRecipient 

46from camcops_server.cc_modules.cc_exportrecipientinfo import ( 

47 ExportRecipientInfo, 

48) 

49from camcops_server.cc_modules.cc_fhir import ( 

50 fhir_reference_from_identifier, 

51 fhir_sysval_from_id, 

52 FhirExportException, 

53 FhirTaskExporter, 

54) 

55from camcops_server.cc_modules.cc_pyramid import Routes 

56from camcops_server.cc_modules.cc_unittest import DemoDatabaseTestCase 

57from camcops_server.cc_modules.cc_version_string import ( 

58 CAMCOPS_SERVER_VERSION_STRING, 

59) 

60from camcops_server.tasks.apeqpt import Apeqpt 

61from camcops_server.tasks.bmi import Bmi 

62from camcops_server.tasks.gad7 import Gad7 

63from camcops_server.tasks.diagnosis import ( 

64 DiagnosisIcd10, 

65 DiagnosisIcd10Item, 

66 DiagnosisIcd9CM, 

67 DiagnosisIcd9CMItem, 

68) 

69from camcops_server.tasks.phq9 import Phq9 

70 

71log = logging.getLogger() 

72 

73 

74# ============================================================================= 

75# Constants 

76# ============================================================================= 

77 

78TEST_NHS_NUMBER = generate_random_nhs_number() 

79TEST_RIO_NUMBER = 12345 

80TEST_FORENAME = "Gwendolyn" 

81TEST_SURNAME = "Ryann" 

82TEST_SEX = "F" 

83 

84 

85# ============================================================================= 

86# Helper classes 

87# ============================================================================= 

88 

89 

90class MockFhirTaskExporter(FhirTaskExporter): 

91 pass 

92 

93 

94class MockFhirResponse(mock.Mock): 

95 def __init__(self, response_json: Dict): 

96 super().__init__( 

97 text=json.dumps(response_json), 

98 json=mock.Mock(return_value=response_json), 

99 ) 

100 

101 

102class FhirExportTestCase(DemoDatabaseTestCase): 

103 def setUp(self) -> None: 

104 super().setUp() 

105 recipientinfo = ExportRecipientInfo() 

106 

107 self.recipient = ExportRecipient(recipientinfo) 

108 self.recipient.primary_idnum = self.rio_iddef.which_idnum 

109 self.recipient.fhir_api_url = "https://www.example.com/fhir" 

110 

111 # auto increment doesn't work for BigInteger with SQLite 

112 self.recipient.id = 1 

113 self.recipient.recipient_name = "test" 

114 

115 self.camcops_root_url = self.req.route_url(Routes.HOME).rstrip("/") 

116 # ... no trailing slash 

117 

118 def create_fhir_patient(self) -> None: 

119 self.patient = self.create_patient( 

120 forename=TEST_FORENAME, surname=TEST_SURNAME, sex=TEST_SEX 

121 ) 

122 self.patient_nhs = self.create_patient_idnum( 

123 patient_id=self.patient.id, 

124 which_idnum=self.nhs_iddef.which_idnum, 

125 idnum_value=TEST_NHS_NUMBER, 

126 ) 

127 self.patient_rio = self.create_patient_idnum( 

128 patient_id=self.patient.id, 

129 which_idnum=self.rio_iddef.which_idnum, 

130 idnum_value=TEST_RIO_NUMBER, 

131 ) 

132 

133 

134# ============================================================================= 

135# A generic patient-based task: PHQ9 

136# ============================================================================= 

137 

138 

139class FhirTaskExporterPhq9Tests(FhirExportTestCase): 

140 def create_tasks(self) -> None: 

141 self.create_fhir_patient() 

142 

143 self.task = Phq9() 

144 self.apply_standard_task_fields(self.task) 

145 self.task.q1 = 0 

146 self.task.q2 = 1 

147 self.task.q3 = 2 

148 self.task.q4 = 3 

149 self.task.q5 = 0 

150 self.task.q6 = 1 

151 self.task.q7 = 2 

152 self.task.q8 = 3 

153 self.task.q9 = 0 

154 self.task.q10 = 3 

155 self.task.patient_id = self.patient.id 

156 self.task.save_with_next_available_id( 

157 self.req, self.patient._device_id 

158 ) 

159 self.dbsession.commit() 

160 

161 def test_patient_exported(self) -> None: 

162 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

163 exported_task_fhir = ExportedTaskFhir(exported_task) 

164 

165 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

166 

167 response_json = {Fc.TYPE: Fc.TRANSACTION_RESPONSE} 

168 

169 with mock.patch.object( 

170 exporter.client.server, 

171 "post_json", 

172 return_value=MockFhirResponse(response_json), 

173 ) as mock_post: 

174 exporter.export_task() 

175 

176 args, kwargs = mock_post.call_args 

177 

178 sent_json = args[1] 

179 

180 self.assertEqual(sent_json[Fc.RESOURCE_TYPE], Fc.RESOURCE_TYPE_BUNDLE) 

181 self.assertEqual(sent_json[Fc.TYPE], Fc.TRANSACTION) 

182 

183 patient = sent_json[Fc.ENTRY][0][Fc.RESOURCE] 

184 self.assertEqual(patient[Fc.RESOURCE_TYPE], Fc.RESOURCE_TYPE_PATIENT) 

185 

186 identifier = patient[Fc.IDENTIFIER] 

187 idnum_value = self.patient_rio.idnum_value 

188 

189 patient_id = self.patient.get_fhir_identifier(self.req, self.recipient) 

190 

191 self.assertEqual(identifier[0][Fc.SYSTEM], patient_id.system) 

192 self.assertEqual(identifier[0][Fc.VALUE], str(idnum_value)) 

193 

194 self.assertEqual( 

195 patient[Fc.NAME][0][Fc.NAME_FAMILY], self.patient.surname 

196 ) 

197 self.assertEqual( 

198 patient[Fc.NAME][0][Fc.NAME_GIVEN], [self.patient.forename] 

199 ) 

200 self.assertEqual(patient[Fc.GENDER], Fc.GENDER_FEMALE) 

201 

202 request = sent_json[Fc.ENTRY][0][Fc.REQUEST] 

203 self.assertEqual(request[Fc.METHOD], HttpMethod.POST) 

204 self.assertEqual(request[Fc.URL], Fc.RESOURCE_TYPE_PATIENT) 

205 self.assertEqual( 

206 request[Fc.IF_NONE_EXIST], 

207 fhir_reference_from_identifier(patient_id), 

208 ) 

209 

210 def test_questionnaire_exported(self) -> None: 

211 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

212 exported_task_fhir = ExportedTaskFhir(exported_task) 

213 

214 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

215 

216 response_json = {Fc.TYPE: Fc.TRANSACTION_RESPONSE} 

217 

218 with mock.patch.object( 

219 exporter.client.server, 

220 "post_json", 

221 return_value=MockFhirResponse(response_json), 

222 ) as mock_post: 

223 exporter.export_task() 

224 

225 args, kwargs = mock_post.call_args 

226 

227 sent_json = args[1] 

228 

229 questionnaire = sent_json[Fc.ENTRY][1][Fc.RESOURCE] 

230 self.assertEqual( 

231 questionnaire[Fc.RESOURCE_TYPE], Fc.RESOURCE_TYPE_QUESTIONNAIRE 

232 ) 

233 self.assertEqual(questionnaire[Fc.STATUS], Fc.QSTATUS_ACTIVE) 

234 

235 identifier = questionnaire[Fc.IDENTIFIER] 

236 

237 questionnaire_url = ( 

238 f"{self.camcops_root_url}/{Routes.FHIR_QUESTIONNAIRE_SYSTEM}" 

239 ) 

240 self.assertEqual(identifier[0][Fc.SYSTEM], questionnaire_url) 

241 self.assertEqual( 

242 identifier[0][Fc.VALUE], f"phq9/{CAMCOPS_SERVER_VERSION_STRING}" 

243 ) 

244 

245 question_1 = questionnaire[Fc.ITEM][0] 

246 question_10 = questionnaire[Fc.ITEM][9] 

247 self.assertEqual(question_1[Fc.LINK_ID], "q1") 

248 self.assertEqual( 

249 question_1[Fc.TEXT], 

250 "1. Little interest or pleasure in doing things", 

251 ) 

252 self.assertEqual(question_1[Fc.TYPE], Fc.QITEM_TYPE_CHOICE) 

253 

254 options = question_1[Fc.ANSWER_OPTION] 

255 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.CODE], "0") 

256 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.DISPLAY], "Not at all") 

257 

258 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.CODE], "1") 

259 self.assertEqual( 

260 options[1][Fc.VALUE_CODING][Fc.DISPLAY], "Several days" 

261 ) 

262 

263 self.assertEqual(options[2][Fc.VALUE_CODING][Fc.CODE], "2") 

264 self.assertEqual( 

265 options[2][Fc.VALUE_CODING][Fc.DISPLAY], "More than half the days" 

266 ) 

267 

268 self.assertEqual(options[3][Fc.VALUE_CODING][Fc.CODE], "3") 

269 self.assertEqual( 

270 options[3][Fc.VALUE_CODING][Fc.DISPLAY], "Nearly every day" 

271 ) 

272 

273 self.assertEqual(question_10[Fc.LINK_ID], "q10") 

274 self.assertEqual( 

275 question_10[Fc.TEXT], 

276 ( 

277 "10. If you checked off any problems, how difficult have " 

278 "these problems made it for you to do your work, take care of " 

279 "things at home, or get along with other people?" 

280 ), 

281 ) 

282 self.assertEqual(question_10[Fc.TYPE], Fc.QITEM_TYPE_CHOICE) 

283 options = question_10[Fc.ANSWER_OPTION] 

284 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.CODE], "0") 

285 self.assertEqual( 

286 options[0][Fc.VALUE_CODING][Fc.DISPLAY], "Not difficult at all" 

287 ) 

288 

289 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.CODE], "1") 

290 self.assertEqual( 

291 options[1][Fc.VALUE_CODING][Fc.DISPLAY], "Somewhat difficult" 

292 ) 

293 

294 self.assertEqual(options[2][Fc.VALUE_CODING][Fc.CODE], "2") 

295 self.assertEqual( 

296 options[2][Fc.VALUE_CODING][Fc.DISPLAY], "Very difficult" 

297 ) 

298 

299 self.assertEqual(options[3][Fc.VALUE_CODING][Fc.CODE], "3") 

300 self.assertEqual( 

301 options[3][Fc.VALUE_CODING][Fc.DISPLAY], "Extremely difficult" 

302 ) 

303 

304 self.assertEqual(len(questionnaire[Fc.ITEM]), 10) 

305 

306 request = sent_json[Fc.ENTRY][1][Fc.REQUEST] 

307 self.assertEqual(request[Fc.METHOD], HttpMethod.POST) 

308 self.assertEqual(request[Fc.URL], Fc.RESOURCE_TYPE_QUESTIONNAIRE) 

309 q_id = self.task._get_fhir_questionnaire_id(self.req) 

310 self.assertEqual( 

311 request[Fc.IF_NONE_EXIST], fhir_reference_from_identifier(q_id) 

312 ) 

313 

314 def test_questionnaire_response_exported(self) -> None: 

315 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

316 exported_task_fhir = ExportedTaskFhir(exported_task) 

317 

318 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

319 

320 response_json = {Fc.TYPE: Fc.TRANSACTION_RESPONSE} 

321 

322 with mock.patch.object( 

323 exporter.client.server, 

324 "post_json", 

325 return_value=MockFhirResponse(response_json), 

326 ) as mock_post: 

327 exporter.export_task() 

328 

329 args, kwargs = mock_post.call_args 

330 

331 sent_json = args[1] 

332 

333 response = sent_json[Fc.ENTRY][2][Fc.RESOURCE] 

334 self.assertEqual( 

335 response[Fc.RESOURCE_TYPE], Fc.RESOURCE_TYPE_QUESTIONNAIRE_RESPONSE 

336 ) 

337 

338 q_id = self.task._get_fhir_questionnaire_id(self.req) 

339 self.assertEqual(response[Fc.QUESTIONNAIRE], fhir_sysval_from_id(q_id)) 

340 self.assertEqual( 

341 response[Fc.AUTHORED], self.task.when_created.isoformat() 

342 ) 

343 self.assertEqual(response[Fc.STATUS], Fc.QSTATUS_COMPLETED) 

344 

345 subject = response[Fc.SUBJECT] 

346 identifier = subject[Fc.IDENTIFIER] 

347 self.assertEqual(subject[Fc.TYPE], Fc.RESOURCE_TYPE_PATIENT) 

348 idnum_value = self.patient_rio.idnum_value 

349 

350 patient_id = self.patient.get_fhir_identifier(self.req, self.recipient) 

351 if isinstance(identifier, list): 

352 test_identifier = identifier[0] 

353 else: # only one 

354 test_identifier = identifier 

355 self.assertEqual(test_identifier[Fc.SYSTEM], patient_id.system) 

356 self.assertEqual(test_identifier[Fc.VALUE], str(idnum_value)) 

357 

358 request = sent_json[Fc.ENTRY][2][Fc.REQUEST] 

359 self.assertEqual(request[Fc.METHOD], HttpMethod.POST) 

360 self.assertEqual( 

361 request[Fc.URL], Fc.RESOURCE_TYPE_QUESTIONNAIRE_RESPONSE 

362 ) 

363 qr_id = self.task._get_fhir_questionnaire_response_id(self.req) 

364 self.assertEqual( 

365 request[Fc.IF_NONE_EXIST], fhir_reference_from_identifier(qr_id) 

366 ) 

367 

368 item_1 = response[Fc.ITEM][0] 

369 item_10 = response[Fc.ITEM][9] 

370 self.assertEqual(item_1[Fc.LINK_ID], "q1") 

371 self.assertEqual( 

372 item_1[Fc.TEXT], "1. Little interest or pleasure in doing things" 

373 ) 

374 answer_1 = item_1[Fc.ANSWER][0] 

375 # noinspection PyUnresolvedReferences 

376 self.assertEqual(answer_1[Fc.VALUE_INTEGER], self.task.q1) 

377 

378 self.assertEqual(item_10[Fc.LINK_ID], "q10") 

379 self.assertEqual( 

380 item_10[Fc.TEXT], 

381 ( 

382 "10. If you checked off any problems, how difficult have " 

383 "these problems made it for you to do your work, take care of " 

384 "things at home, or get along with other people?" 

385 ), 

386 ) 

387 answer_10 = item_10[Fc.ANSWER][0] 

388 self.assertEqual(answer_10[Fc.VALUE_INTEGER], self.task.q10) 

389 

390 self.assertEqual(len(response[Fc.ITEM]), 10) 

391 

392 # noinspection PyUnresolvedReferences 

393 def test_exported_task_saved(self) -> None: 

394 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

395 # auto increment doesn't work for BigInteger with SQLite 

396 exported_task.id = 1 

397 self.dbsession.add(exported_task) 

398 

399 exported_task_fhir = ExportedTaskFhir(exported_task) 

400 self.dbsession.add(exported_task_fhir) 

401 

402 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

403 

404 response_json = { 

405 Fc.RESOURCE_TYPE: Fc.RESOURCE_TYPE_BUNDLE, 

406 Fc.ID: "cae48957-e7e6-4649-97f8-0a882076ad0a", 

407 Fc.TYPE: Fc.TRANSACTION_RESPONSE, 

408 Fc.LINK: [ 

409 {Fc.RELATION: Fc.SELF, Fc.URL: "http://localhost:8080/fhir"} 

410 ], 

411 Fc.ENTRY: [ 

412 { 

413 Fc.RESPONSE: { 

414 Fc.STATUS: Fc.RESPONSE_STATUS_200_OK, 

415 Fc.LOCATION: "Patient/1/_history/1", 

416 Fc.ETAG: "1", 

417 } 

418 }, 

419 { 

420 Fc.RESPONSE: { 

421 Fc.STATUS: Fc.RESPONSE_STATUS_200_OK, 

422 Fc.LOCATION: "Questionnaire/26/_history/1", 

423 Fc.ETAG: "1", 

424 } 

425 }, 

426 { 

427 Fc.RESPONSE: { 

428 Fc.STATUS: Fc.RESPONSE_STATUS_201_CREATED, 

429 Fc.LOCATION: "QuestionnaireResponse/42/_history/1", 

430 Fc.ETAG: "1", 

431 Fc.LAST_MODIFIED: "2021-05-24T09:30:11.098+00:00", 

432 } 

433 }, 

434 ], 

435 } 

436 

437 with mock.patch.object( 

438 exporter.client.server, 

439 "post_json", 

440 return_value=MockFhirResponse(response_json), 

441 ): 

442 exporter.export_task() 

443 

444 self.dbsession.commit() 

445 

446 entries = ( 

447 exported_task_fhir.entries 

448 ) # type: List[ExportedTaskFhirEntry] # noqa 

449 

450 entries.sort(key=lambda e: e.location) 

451 

452 self.assertEqual(entries[0].status, Fc.RESPONSE_STATUS_200_OK) 

453 self.assertEqual(entries[0].location, "Patient/1/_history/1") 

454 self.assertEqual(entries[0].etag, "1") 

455 

456 self.assertEqual(entries[1].status, Fc.RESPONSE_STATUS_200_OK) 

457 self.assertEqual(entries[1].location, "Questionnaire/26/_history/1") 

458 self.assertEqual(entries[1].etag, "1") 

459 

460 self.assertEqual(entries[2].status, Fc.RESPONSE_STATUS_201_CREATED) 

461 self.assertEqual( 

462 entries[2].location, "QuestionnaireResponse/42/_history/1" 

463 ) 

464 self.assertEqual(entries[2].etag, "1") 

465 self.assertEqual( 

466 entries[2].last_modified, 

467 datetime.datetime(2021, 5, 24, 9, 30, 11, 98000), 

468 ) 

469 

470 def test_raises_when_http_error(self) -> None: 

471 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

472 exported_task_fhir = ExportedTaskFhir(exported_task) 

473 

474 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

475 

476 errmsg = "Something bad happened" 

477 with mock.patch.object( 

478 exporter.client.server, 

479 "post_json", 

480 side_effect=HTTPError(response=mock.Mock(text=errmsg)), 

481 ): 

482 with self.assertRaises(FhirExportException) as cm: 

483 exporter.export_task() 

484 

485 message = str(cm.exception) 

486 self.assertIn(errmsg, message) 

487 

488 def test_raises_when_fhirclient_raises(self) -> None: 

489 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

490 exported_task_fhir = ExportedTaskFhir(exported_task) 

491 

492 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

493 

494 exporter.client.server = None 

495 with self.assertRaises(FhirExportException) as cm: 

496 exporter.export_task() 

497 

498 message = str(cm.exception) 

499 self.assertIn("Cannot create a resource without a server", message) 

500 

501 def test_raises_for_missing_api_url(self) -> None: 

502 self.recipient.fhir_api_url = "" 

503 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

504 exported_task_fhir = ExportedTaskFhir(exported_task) 

505 

506 with self.assertRaises(FhirExportException) as cm: 

507 FhirTaskExporter(self.req, exported_task_fhir) 

508 

509 message = str(cm.exception) 

510 self.assertIn("must be initialized with `base_uri`", message) 

511 

512 

513# ============================================================================= 

514# A generic anonymous task: APEQPT 

515# ============================================================================= 

516 

517APEQPT_Q_WHEN = "Date and time the assessment tool was completed" 

518OFFERED_PREFERENCE = "Have you been offered your preference?" 

519SATISFIED_ASSESSMENT = "How satisfied were you with your assessment?" 

520TELL_US = ( 

521 "Please use this space to tell us about your experience of our service." 

522) 

523PREFER_ANY = "Do you prefer any of the treatments among the options available?" 

524GIVEN_INFO = ( 

525 "Were you given information about options for choosing a " 

526 "treatment that is appropriate for your problems?" 

527) 

528APEQ_SATIS_A4 = "Completely satisfied" 

529APEQ_SATIS_A3 = "Mostly satisfied" 

530APEQ_SATIS_A2 = "Neither satisfied nor dissatisfied" 

531APEQ_SATIS_A1 = "Not satisfied" 

532APEQ_SATIS_A0 = "Not at all satisfied" 

533 

534 

535class FhirTaskExporterAnonymousTests(FhirExportTestCase): 

536 def create_tasks(self) -> None: 

537 self.task = Apeqpt() 

538 self.apply_standard_task_fields(self.task) 

539 self.task.q_datetime = pendulum.now() 

540 self.task.q1_choice = 0 

541 self.task.q2_choice = 1 

542 self.task.q3_choice = 2 

543 self.task.q1_satisfaction = 3 

544 self.task.q2_satisfaction = "Service experience" 

545 

546 self.task.save_with_next_available_id(self.req, self.server_device.id) 

547 self.dbsession.commit() 

548 

549 def test_questionnaire_exported(self) -> None: 

550 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

551 exported_task_fhir = ExportedTaskFhir(exported_task) 

552 

553 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

554 

555 response_json = {Fc.TYPE: Fc.TRANSACTION_RESPONSE} 

556 

557 with mock.patch.object( 

558 exporter.client.server, 

559 "post_json", 

560 return_value=MockFhirResponse(response_json), 

561 ) as mock_post: 

562 exporter.export_task() 

563 

564 args, kwargs = mock_post.call_args 

565 

566 sent_json = args[1] 

567 

568 questionnaire = sent_json[Fc.ENTRY][0][Fc.RESOURCE] 

569 self.assertEqual( 

570 questionnaire[Fc.RESOURCE_TYPE], Fc.RESOURCE_TYPE_QUESTIONNAIRE 

571 ) 

572 self.assertEqual(questionnaire[Fc.STATUS], Fc.QSTATUS_ACTIVE) 

573 

574 identifier = questionnaire[Fc.IDENTIFIER] 

575 

576 questionnaire_url = ( 

577 f"{self.camcops_root_url}/{Routes.FHIR_QUESTIONNAIRE_SYSTEM}" 

578 ) 

579 self.assertEqual(identifier[0][Fc.SYSTEM], questionnaire_url) 

580 self.assertEqual( 

581 identifier[0][Fc.VALUE], f"apeqpt/{CAMCOPS_SERVER_VERSION_STRING}" 

582 ) 

583 

584 self.assertEqual(len(questionnaire[Fc.ITEM]), 5) 

585 ( 

586 q1_choice, 

587 q2_choice, 

588 q3_choice, 

589 q1_satisfaction, 

590 q2_satisfaction, 

591 ) = questionnaire[Fc.ITEM] 

592 

593 # q1_choice 

594 self.assertEqual(q1_choice[Fc.LINK_ID], "q1_choice") 

595 self.assertEqual(q1_choice[Fc.TEXT], GIVEN_INFO) 

596 self.assertEqual(q1_choice[Fc.TYPE], Fc.QITEM_TYPE_CHOICE) 

597 

598 options = q1_choice[Fc.ANSWER_OPTION] 

599 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.CODE], "0") 

600 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.DISPLAY], "No") 

601 

602 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.CODE], "1") 

603 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.DISPLAY], "Yes") 

604 

605 # q2_choice 

606 self.assertEqual(q2_choice[Fc.LINK_ID], "q2_choice") 

607 self.assertEqual(q2_choice[Fc.TEXT], PREFER_ANY) 

608 self.assertEqual(q2_choice[Fc.TYPE], Fc.QITEM_TYPE_CHOICE) 

609 options = q2_choice[Fc.ANSWER_OPTION] 

610 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.CODE], "0") 

611 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.DISPLAY], "No") 

612 

613 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.CODE], "1") 

614 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.DISPLAY], "Yes") 

615 

616 # q3_choice 

617 self.assertEqual(q3_choice[Fc.LINK_ID], "q3_choice") 

618 self.assertEqual(q3_choice[Fc.TEXT], OFFERED_PREFERENCE) 

619 self.assertEqual(q3_choice[Fc.TYPE], Fc.QITEM_TYPE_CHOICE) 

620 options = q3_choice[Fc.ANSWER_OPTION] 

621 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.CODE], "0") 

622 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.DISPLAY], "No") 

623 

624 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.CODE], "1") 

625 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.DISPLAY], "Yes") 

626 

627 self.assertEqual(options[2][Fc.VALUE_CODING][Fc.CODE], "2") 

628 self.assertEqual(options[2][Fc.VALUE_CODING][Fc.DISPLAY], "N/A") 

629 

630 # q1_satisfaction 

631 self.assertEqual(q1_satisfaction[Fc.LINK_ID], "q1_satisfaction") 

632 self.assertEqual(q1_satisfaction[Fc.TEXT], SATISFIED_ASSESSMENT) 

633 self.assertEqual(q1_satisfaction[Fc.TYPE], Fc.QITEM_TYPE_CHOICE) 

634 options = q1_satisfaction[Fc.ANSWER_OPTION] 

635 self.assertEqual(options[0][Fc.VALUE_CODING][Fc.CODE], "0") 

636 self.assertEqual( 

637 options[0][Fc.VALUE_CODING][Fc.DISPLAY], APEQ_SATIS_A0 

638 ) 

639 

640 self.assertEqual(options[1][Fc.VALUE_CODING][Fc.CODE], "1") 

641 self.assertEqual( 

642 options[1][Fc.VALUE_CODING][Fc.DISPLAY], APEQ_SATIS_A1 

643 ) 

644 

645 self.assertEqual(options[2][Fc.VALUE_CODING][Fc.CODE], "2") 

646 self.assertEqual( 

647 options[2][Fc.VALUE_CODING][Fc.DISPLAY], APEQ_SATIS_A2 

648 ) 

649 

650 self.assertEqual(options[3][Fc.VALUE_CODING][Fc.CODE], "3") 

651 self.assertEqual( 

652 options[3][Fc.VALUE_CODING][Fc.DISPLAY], APEQ_SATIS_A3 

653 ) 

654 

655 self.assertEqual(options[4][Fc.VALUE_CODING][Fc.CODE], "4") 

656 self.assertEqual( 

657 options[4][Fc.VALUE_CODING][Fc.DISPLAY], APEQ_SATIS_A4 

658 ) 

659 

660 # q2 satisfaction 

661 self.assertEqual(q2_satisfaction[Fc.LINK_ID], "q2_satisfaction") 

662 self.assertEqual(q2_satisfaction[Fc.TEXT], TELL_US) 

663 self.assertEqual(q2_satisfaction[Fc.TYPE], Fc.QITEM_TYPE_STRING) 

664 

665 request = sent_json[Fc.ENTRY][0][Fc.REQUEST] 

666 self.assertEqual(request[Fc.METHOD], HttpMethod.POST) 

667 self.assertEqual(request[Fc.URL], Fc.RESOURCE_TYPE_QUESTIONNAIRE) 

668 q_id = self.task._get_fhir_questionnaire_id(self.req) 

669 self.assertEqual( 

670 request[Fc.IF_NONE_EXIST], fhir_reference_from_identifier(q_id) 

671 ) 

672 

673 def test_questionnaire_response_exported(self) -> None: 

674 exported_task = ExportedTask(task=self.task, recipient=self.recipient) 

675 exported_task_fhir = ExportedTaskFhir(exported_task) 

676 

677 exporter = MockFhirTaskExporter(self.req, exported_task_fhir) 

678 

679 response_json = {Fc.TYPE: Fc.TRANSACTION_RESPONSE} 

680 

681 with mock.patch.object( 

682 exporter.client.server, 

683 "post_json", 

684 return_value=MockFhirResponse(response_json), 

685 ) as mock_post: 

686 exporter.export_task() 

687 

688 args, kwargs = mock_post.call_args 

689 

690 sent_json = args[1] 

691 

692 response = sent_json[Fc.ENTRY][1][Fc.RESOURCE] 

693 self.assertEqual( 

694 response[Fc.RESOURCE_TYPE], Fc.RESOURCE_TYPE_QUESTIONNAIRE_RESPONSE 

695 ) 

696 q_id = self.task._get_fhir_questionnaire_id(self.req) 

697 self.assertEqual(response[Fc.QUESTIONNAIRE], fhir_sysval_from_id(q_id)) 

698 self.assertEqual( 

699 response[Fc.AUTHORED], self.task.when_created.isoformat() 

700 ) 

701 self.assertEqual(response[Fc.STATUS], Fc.QSTATUS_COMPLETED) 

702 

703 request = sent_json[Fc.ENTRY][1][Fc.REQUEST] 

704 self.assertEqual(request[Fc.METHOD], HttpMethod.POST) 

705 self.assertEqual(request[Fc.URL], "QuestionnaireResponse") 

706 qr_id = self.task._get_fhir_questionnaire_response_id(self.req) 

707 self.assertEqual( 

708 request[Fc.IF_NONE_EXIST], fhir_reference_from_identifier(qr_id) 

709 ) 

710 

711 self.assertEqual(len(response[Fc.ITEM]), 5) 

712 ( 

713 q1_choice, 

714 q2_choice, 

715 q3_choice, 

716 q1_satisfaction, 

717 q2_satisfaction, 

718 ) = response[Fc.ITEM] 

719 

720 # q1_choice 

721 self.assertEqual(q1_choice[Fc.LINK_ID], "q1_choice") 

722 self.assertEqual(q1_choice[Fc.TEXT], GIVEN_INFO) 

723 q1_choice_answer = q1_choice[Fc.ANSWER][0] 

724 self.assertEqual( 

725 q1_choice_answer[Fc.VALUE_INTEGER], self.task.q1_choice 

726 ) 

727 

728 # q2_choice 

729 self.assertEqual(q2_choice[Fc.LINK_ID], "q2_choice") 

730 self.assertEqual(q2_choice[Fc.TEXT], PREFER_ANY) 

731 q2_choice_answer = q2_choice[Fc.ANSWER][0] 

732 self.assertEqual( 

733 q2_choice_answer[Fc.VALUE_INTEGER], self.task.q2_choice 

734 ) 

735 

736 # q3_choice 

737 self.assertEqual(q3_choice[Fc.LINK_ID], "q3_choice") 

738 self.assertEqual(q3_choice[Fc.TEXT], OFFERED_PREFERENCE) 

739 q3_choice_answer = q3_choice[Fc.ANSWER][0] 

740 self.assertEqual( 

741 q3_choice_answer[Fc.VALUE_INTEGER], self.task.q3_choice 

742 ) 

743 

744 # q1_satisfaction 

745 self.assertEqual(q1_satisfaction[Fc.LINK_ID], "q1_satisfaction") 

746 self.assertEqual(q1_satisfaction[Fc.TEXT], SATISFIED_ASSESSMENT) 

747 q1_satisfaction_answer = q1_satisfaction[Fc.ANSWER][0] 

748 self.assertEqual( 

749 q1_satisfaction_answer[Fc.VALUE_INTEGER], self.task.q1_satisfaction 

750 ) 

751 

752 # q2 satisfaction 

753 self.assertEqual(q2_satisfaction[Fc.LINK_ID], "q2_satisfaction") 

754 self.assertEqual(q2_satisfaction[Fc.TEXT], TELL_US) 

755 q2_satisfaction_answer = q2_satisfaction[Fc.ANSWER][0] 

756 self.assertEqual( 

757 q2_satisfaction_answer[Fc.VALUE_STRING], self.task.q2_satisfaction 

758 ) 

759 

760 

761# ============================================================================= 

762# Tasks that add their own special details 

763# ============================================================================= 

764 

765 

766class FhirTaskExporterBMITests(FhirExportTestCase): 

767 def create_tasks(self) -> None: 

768 self.create_fhir_patient() 

769 

770 self.task = Bmi() 

771 self.apply_standard_task_fields(self.task) 

772 self.task.mass_kg = 70 

773 self.task.height_m = 1.8 

774 self.task.waist_cm = 82 

775 self.task.patient_id = self.patient.id 

776 self.task.save_with_next_available_id( 

777 self.req, self.patient._device_id 

778 ) 

779 self.dbsession.commit() 

780 

781 def test_observations(self) -> None: 

782 bundle = self.task.get_fhir_bundle( 

783 self.req, self.recipient, skip_docs_if_other_content=True 

784 ) 

785 bundle_str = json.dumps(bundle.as_json(), indent=JSON_INDENT) 

786 log.debug(f"Bundle:\n{bundle_str}") 

787 # The test is that it doesn't crash. 

788 

789 

790class FhirTaskExporterDiagnosisIcd10Tests(FhirExportTestCase): 

791 def create_tasks(self) -> None: 

792 self.create_fhir_patient() 

793 

794 self.task = DiagnosisIcd10() 

795 self.apply_standard_task_fields(self.task) 

796 self.task.patient_id = self.patient.id 

797 self.task.save_with_next_available_id( 

798 self.req, self.patient._device_id 

799 ) 

800 self.dbsession.commit() 

801 

802 # noinspection PyArgumentList 

803 item1 = DiagnosisIcd10Item( 

804 diagnosis_icd10_id=self.task.id, 

805 seqnum=1, 

806 code="F33.30", 

807 description="Recurrent depressive disorder, current episode " 

808 "severe with psychotic symptoms: " 

809 "with mood-congruent psychotic symptoms", 

810 comment="Cotard's syndrome", 

811 ) 

812 self.apply_standard_db_fields(item1) 

813 item1.save_with_next_available_id(self.req, self.task._device_id) 

814 # noinspection PyArgumentList 

815 item2 = DiagnosisIcd10Item( 

816 diagnosis_icd10_id=self.task.id, 

817 seqnum=2, 

818 code="F43.1", 

819 description="Post-traumatic stress disorder", 

820 ) 

821 self.apply_standard_db_fields(item2) 

822 item2.save_with_next_available_id(self.req, self.task._device_id) 

823 

824 def test_observations(self) -> None: 

825 bundle = self.task.get_fhir_bundle( 

826 self.req, self.recipient, skip_docs_if_other_content=True 

827 ) 

828 bundle_str = json.dumps(bundle.as_json(), indent=JSON_INDENT) 

829 log.debug(f"Bundle:\n{bundle_str}") 

830 # The test is that it doesn't crash. 

831 

832 

833class FhirTaskExporterDiagnosisIcd9CMTests(FhirExportTestCase): 

834 def create_tasks(self) -> None: 

835 self.create_fhir_patient() 

836 

837 self.task = DiagnosisIcd9CM() 

838 self.apply_standard_task_fields(self.task) 

839 self.task.patient_id = self.patient.id 

840 self.task.save_with_next_available_id( 

841 self.req, self.patient._device_id 

842 ) 

843 self.dbsession.commit() 

844 

845 # noinspection PyArgumentList 

846 item1 = DiagnosisIcd9CMItem( 

847 diagnosis_icd9cm_id=self.task.id, 

848 seqnum=1, 

849 code="290.4", 

850 description="Vascular dementia", 

851 comment="or perhaps mixed dementia", 

852 ) 

853 self.apply_standard_db_fields(item1) 

854 item1.save_with_next_available_id(self.req, self.task._device_id) 

855 # noinspection PyArgumentList 

856 item2 = DiagnosisIcd9CMItem( 

857 diagnosis_icd9cm_id=self.task.id, 

858 seqnum=2, 

859 code="303.0", 

860 description="Acute alcoholic intoxication", 

861 ) 

862 self.apply_standard_db_fields(item2) 

863 item2.save_with_next_available_id(self.req, self.task._device_id) 

864 

865 def test_observations(self) -> None: 

866 bundle = self.task.get_fhir_bundle( 

867 self.req, self.recipient, skip_docs_if_other_content=True 

868 ) 

869 bundle_str = json.dumps(bundle.as_json(), indent=JSON_INDENT) 

870 log.debug(f"Bundle:\n{bundle_str}") 

871 # The test is that it doesn't crash. 

872 

873 

874class FhirTaskExporterGad7Tests(FhirExportTestCase): 

875 """ 

876 The GAD7 is a standard questionnaire that we don't provide any special 

877 FHIR support for; we rely on autodiscovery. 

878 """ 

879 

880 def create_tasks(self) -> None: 

881 self.create_fhir_patient() 

882 

883 self.task = Gad7() 

884 self.apply_standard_task_fields(self.task) 

885 self.task.patient_id = self.patient.id 

886 self.task.save_with_next_available_id( 

887 self.req, self.patient._device_id 

888 ) 

889 self.dbsession.commit() 

890 

891 def test_observations(self) -> None: 

892 bundle = self.task.get_fhir_bundle( 

893 self.req, self.recipient, skip_docs_if_other_content=True 

894 ) 

895 bundle_str = json.dumps(bundle.as_json(), indent=JSON_INDENT) 

896 log.critical(f"Bundle:\n{bundle_str}") 

897 # The test is that it doesn't crash.