Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

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

4 

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

6 

7 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of CamCOPS. 

10 

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

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

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

14 (at your option) any later version. 

15 

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

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

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

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

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

23 

24=============================================================================== 

25""" 

26import os 

27import tempfile 

28from typing import Generator, TYPE_CHECKING 

29from unittest import mock, TestCase 

30 

31from pandas import DataFrame 

32import pendulum 

33import redcap 

34 

35from camcops_server.cc_modules.cc_constants import ConfigParamExportRecipient 

36from camcops_server.cc_modules.cc_exportrecipient import ExportRecipient 

37from camcops_server.cc_modules.cc_exportrecipientinfo import ExportRecipientInfo 

38from camcops_server.cc_modules.cc_redcap import ( 

39 MISSING_EVENT_TAG_OR_ATTRIBUTE, 

40 RedcapExportException, 

41 RedcapFieldmap, 

42 RedcapNewRecordUploader, 

43 RedcapRecordStatus, 

44 RedcapTaskExporter, 

45) 

46from camcops_server.cc_modules.cc_unittest import DemoDatabaseTestCase 

47 

48if TYPE_CHECKING: 

49 from camcops_server.cc_modules.cc_patient import Patient 

50 

51 

52# ============================================================================= 

53# Unit testing 

54# ============================================================================= 

55 

56 

57class MockProject(mock.Mock): 

58 def __init__(self, *args, **kwargs) -> None: 

59 super().__init__(*args, **kwargs) 

60 

61 self.export_project_info = mock.Mock() 

62 self.export_records = mock.Mock() 

63 self.generate_next_record_name = mock.Mock() 

64 self.import_file = mock.Mock() 

65 self.import_records = mock.Mock() 

66 self.is_longitudinal = mock.Mock(return_value=False) 

67 

68 

69class MockRedcapTaskExporter(RedcapTaskExporter): 

70 def __init__(self) -> None: 

71 mock_project = MockProject() 

72 self.get_project = mock.Mock(return_value=mock_project) 

73 

74 config = mock.Mock() 

75 self.req = mock.Mock(config=config) 

76 

77 

78class MockRedcapNewRecordUploader(RedcapNewRecordUploader): 

79 # noinspection PyMissingConstructor 

80 def __init__(self) -> None: 

81 self.req = mock.Mock() 

82 self.project = MockProject() 

83 self.task = mock.Mock(tablename="mock_task") 

84 

85 

86class RedcapExporterTests(TestCase): 

87 def test_next_instance_id_converted_to_int(self) -> None: 

88 import numpy 

89 

90 records = DataFrame({ 

91 "record_id": ["1", "1", "1", "1", "1"], 

92 "redcap_repeat_instrument": ["bmi", "bmi", "bmi", "bmi", "bmi"], 

93 "redcap_repeat_instance": [ 

94 numpy.float64(1.0), 

95 numpy.float64(2.0), 

96 numpy.float64(3.0), 

97 numpy.float64(4.0), 

98 numpy.float64(5.0), 

99 ], 

100 

101 }) 

102 

103 next_instance_id = RedcapTaskExporter._get_next_instance_id( 

104 records, "bmi", "record_id", "1" 

105 ) 

106 

107 self.assertEqual(next_instance_id, 6) 

108 self.assertEqual(type(next_instance_id), int) 

109 

110 

111class RedcapExportErrorTests(TestCase): 

112 def test_raises_when_fieldmap_has_unknown_symbols(self) -> None: 

113 exporter = MockRedcapNewRecordUploader() 

114 

115 task = mock.Mock(tablename="bmi") 

116 fieldmap = {"pa_height": "sys.platform"} 

117 

118 field_dict = {} 

119 

120 with self.assertRaises(RedcapExportException) as cm: 

121 exporter.transform_fields(field_dict, task, fieldmap) 

122 

123 message = str(cm.exception) 

124 self.assertIn("Error in formula 'sys.platform':", message) 

125 self.assertIn("Task: 'bmi'", message) 

126 self.assertIn("REDCap field: 'pa_height'", message) 

127 self.assertIn("'sys' is not defined", message) 

128 

129 def test_raises_when_fieldmap_empty_in_config(self) -> None: 

130 

131 exporter = MockRedcapTaskExporter() 

132 

133 recipient = mock.Mock(redcap_fieldmap_filename="") 

134 with self.assertRaises(RedcapExportException) as cm: 

135 exporter.get_fieldmap_filename(recipient) 

136 

137 message = str(cm.exception) 

138 self.assertIn(f"{ConfigParamExportRecipient.REDCAP_FIELDMAP_FILENAME} " 

139 f"is empty in the config file", message) 

140 

141 def test_raises_when_fieldmap_not_set_in_config(self) -> None: 

142 

143 exporter = MockRedcapTaskExporter() 

144 

145 recipient = mock.Mock(redcap_fieldmap_filename=None) 

146 with self.assertRaises(RedcapExportException) as cm: 

147 exporter.get_fieldmap_filename(recipient) 

148 

149 message = str(cm.exception) 

150 self.assertIn(f"{ConfigParamExportRecipient.REDCAP_FIELDMAP_FILENAME} " 

151 f"is not set in the config file", message) 

152 

153 def test_raises_when_error_from_redcap_on_import(self) -> None: 

154 exporter = MockRedcapNewRecordUploader() 

155 exporter.project.import_records.side_effect = redcap.RedcapError( 

156 "Something went wrong" 

157 ) 

158 

159 with self.assertRaises(RedcapExportException) as cm: 

160 record = {} 

161 exporter.upload_record(record) 

162 message = str(cm.exception) 

163 

164 self.assertIn("Something went wrong", message) 

165 

166 def test_raises_when_error_from_redcap_on_init(self) -> None: 

167 with mock.patch("redcap.project.Project.__init__") as mock_init: 

168 mock_init.side_effect = redcap.RedcapError( 

169 "Something went wrong" 

170 ) 

171 

172 with self.assertRaises(RedcapExportException) as cm: 

173 exporter = RedcapTaskExporter() 

174 recipient = mock.Mock() 

175 exporter.get_project(recipient) 

176 

177 message = str(cm.exception) 

178 

179 self.assertIn("Something went wrong", message) 

180 

181 def test_raises_when_field_not_a_file_field(self) -> None: 

182 exporter = MockRedcapNewRecordUploader() 

183 exporter.project.import_file.side_effect = ValueError( 

184 "Error with file field" 

185 ) 

186 

187 task = mock.Mock() 

188 

189 with self.assertRaises(RedcapExportException) as cm: 

190 record_id = 1 

191 repeat_instance = 1 

192 file_dict = {"medication_items": b"not a real file"} 

193 exporter.upload_files(task, record_id, repeat_instance, file_dict) 

194 message = str(cm.exception) 

195 

196 self.assertIn("Error with file field", message) 

197 

198 def test_raises_when_error_from_redcap_on_import_file(self) -> None: 

199 exporter = MockRedcapNewRecordUploader() 

200 exporter.project.import_file.side_effect = redcap.RedcapError( 

201 "Something went wrong" 

202 ) 

203 

204 task = mock.Mock() 

205 

206 with self.assertRaises(RedcapExportException) as cm: 

207 record_id = 1 

208 repeat_instance = 1 

209 file_dict = {"medication_items": b"not a real file"} 

210 exporter.upload_files(task, record_id, repeat_instance, file_dict) 

211 message = str(cm.exception) 

212 

213 self.assertIn("Something went wrong", message) 

214 

215 

216class RedcapFieldmapTests(TestCase): 

217 def test_raises_when_xml_file_missing(self) -> None: 

218 with self.assertRaises(RedcapExportException) as cm: 

219 RedcapFieldmap("/does/not/exist/bmi.xml") 

220 

221 message = str(cm.exception) 

222 

223 self.assertIn("Unable to open fieldmap file", message) 

224 self.assertIn("bmi.xml", message) 

225 

226 def test_raises_when_fieldmap_missing(self) -> None: 

227 with tempfile.NamedTemporaryFile( 

228 mode="w", suffix="xml") as fieldmap_file: 

229 fieldmap_file.write("""<?xml version="1.0" encoding="UTF-8"?> 

230<someothertag></someothertag> 

231""") 

232 fieldmap_file.flush() 

233 

234 with self.assertRaises(RedcapExportException) as cm: 

235 RedcapFieldmap(fieldmap_file.name) 

236 

237 message = str(cm.exception) 

238 self.assertIn(("Expected the root tag to be 'fieldmap' instead of " 

239 "'someothertag'"), message) 

240 self.assertIn(fieldmap_file.name, message) 

241 

242 def test_raises_when_root_tag_missing(self) -> None: 

243 with tempfile.NamedTemporaryFile( 

244 mode="w", suffix="xml") as fieldmap_file: 

245 fieldmap_file.write("""<?xml version="1.0" encoding="UTF-8"?> 

246""") 

247 fieldmap_file.flush() 

248 

249 with self.assertRaises(RedcapExportException) as cm: 

250 RedcapFieldmap(fieldmap_file.name) 

251 

252 message = str(cm.exception) 

253 self.assertIn("There was a problem parsing", message) 

254 self.assertIn(fieldmap_file.name, message) 

255 

256 def test_raises_when_patient_missing(self) -> None: 

257 with tempfile.NamedTemporaryFile( 

258 mode="w", suffix="xml") as fieldmap_file: 

259 fieldmap_file.write( 

260 """<?xml version="1.0" encoding="UTF-8"?> 

261 <fieldmap> 

262 </fieldmap> 

263 """) 

264 fieldmap_file.flush() 

265 

266 with self.assertRaises(RedcapExportException) as cm: 

267 RedcapFieldmap(fieldmap_file.name) 

268 

269 message = str(cm.exception) 

270 self.assertIn("'patient' is missing from", message) 

271 self.assertIn(fieldmap_file.name, message) 

272 

273 def test_raises_when_patient_missing_attributes(self) -> None: 

274 with tempfile.NamedTemporaryFile( 

275 mode="w", suffix="xml") as fieldmap_file: 

276 fieldmap_file.write( 

277 """<?xml version="1.0" encoding="UTF-8"?> 

278 <fieldmap> 

279 <patient /> 

280 </fieldmap> 

281 """) 

282 fieldmap_file.flush() 

283 

284 with self.assertRaises(RedcapExportException) as cm: 

285 RedcapFieldmap(fieldmap_file.name) 

286 

287 message = str(cm.exception) 

288 self.assertIn( 

289 "'patient' must have attributes: instrument, redcap_field", 

290 message 

291 ) 

292 self.assertIn(fieldmap_file.name, message) 

293 

294 def test_raises_when_record_missing(self) -> None: 

295 with tempfile.NamedTemporaryFile( 

296 mode="w", suffix="xml") as fieldmap_file: 

297 fieldmap_file.write( 

298 """<?xml version="1.0" encoding="UTF-8"?> 

299 <fieldmap> 

300 <patient instrument="patient_record" redcap_field="patient_id" /> 

301 </fieldmap> 

302 """) # noqa: E501 

303 fieldmap_file.flush() 

304 

305 with self.assertRaises(RedcapExportException) as cm: 

306 RedcapFieldmap(fieldmap_file.name) 

307 

308 message = str(cm.exception) 

309 self.assertIn("'record' is missing from", message) 

310 self.assertIn(fieldmap_file.name, message) 

311 

312 def test_raises_when_record_missing_attributes(self) -> None: 

313 with tempfile.NamedTemporaryFile( 

314 mode="w", suffix="xml") as fieldmap_file: 

315 fieldmap_file.write( 

316 """<?xml version="1.0" encoding="UTF-8"?> 

317 <fieldmap> 

318 <patient instrument="patient_record" redcap_field="patient_id" /> 

319 <record /> 

320 </fieldmap> 

321 """) # noqa: E501 

322 fieldmap_file.flush() 

323 

324 with self.assertRaises(RedcapExportException) as cm: 

325 RedcapFieldmap(fieldmap_file.name) 

326 

327 message = str(cm.exception) 

328 self.assertIn( 

329 "'record' must have attributes: instrument, redcap_field", 

330 message 

331 ) 

332 self.assertIn(fieldmap_file.name, message) 

333 

334 def test_raises_when_instruments_missing(self) -> None: 

335 with tempfile.NamedTemporaryFile( 

336 mode="w", suffix="xml") as fieldmap_file: 

337 fieldmap_file.write( 

338 """<?xml version="1.0" encoding="UTF-8"?> 

339 <fieldmap> 

340 <patient instrument="patient_record" redcap_field="patient_id" /> 

341 <record instrument="patient_record" redcap_field="record_id" /> 

342 </fieldmap> 

343 """) # noqa: E501 

344 fieldmap_file.flush() 

345 

346 with self.assertRaises(RedcapExportException) as cm: 

347 RedcapFieldmap(fieldmap_file.name) 

348 

349 message = str(cm.exception) 

350 self.assertIn("'instruments' tag is missing from", message) 

351 self.assertIn(fieldmap_file.name, message) 

352 

353 def test_raises_when_instruments_missing_attributes(self) -> None: 

354 with tempfile.NamedTemporaryFile( 

355 mode="w", suffix="xml") as fieldmap_file: 

356 fieldmap_file.write( 

357 """<?xml version="1.0" encoding="UTF-8"?> 

358 <fieldmap> 

359 <patient instrument="patient_record" redcap_field="patient_id" /> 

360 <record instrument="patient_record" redcap_field="record_id" /> 

361 <instruments> 

362 <instrument /> 

363 </instruments> 

364 </fieldmap> 

365 """) # noqa: E501 

366 fieldmap_file.flush() 

367 

368 with self.assertRaises(RedcapExportException) as cm: 

369 RedcapFieldmap(fieldmap_file.name) 

370 

371 message = str(cm.exception) 

372 self.assertIn( 

373 "'instrument' must have attributes: name, task", 

374 message 

375 ) 

376 self.assertIn(fieldmap_file.name, message) 

377 

378 def test_raises_when_file_fields_missing_attributes(self) -> None: 

379 with tempfile.NamedTemporaryFile( 

380 mode="w", suffix="xml") as fieldmap_file: 

381 fieldmap_file.write( 

382 """<?xml version="1.0" encoding="UTF-8"?> 

383 <fieldmap> 

384 <patient instrument="patient_record" redcap_field="patient_id" /> 

385 <record instrument="patient_record" redcap_field="record_id" /> 

386 <instruments> 

387 <instrument name="bmi" task="bmi"> 

388 <files> 

389 <field /> 

390 </files> 

391 </instrument> 

392 </instruments> 

393 </fieldmap> 

394 """) # noqa: E501 

395 fieldmap_file.flush() 

396 

397 with self.assertRaises(RedcapExportException) as cm: 

398 RedcapFieldmap(fieldmap_file.name) 

399 

400 message = str(cm.exception) 

401 self.assertIn( 

402 "'field' must have attributes: name, formula", 

403 message 

404 ) 

405 self.assertIn(fieldmap_file.name, message) 

406 

407 def test_raises_when_fields_missing_attributes(self) -> None: 

408 with tempfile.NamedTemporaryFile( 

409 mode="w", suffix="xml") as fieldmap_file: 

410 fieldmap_file.write( 

411 """<?xml version="1.0" encoding="UTF-8"?> 

412 <fieldmap> 

413 <patient instrument="patient_record" redcap_field="patient_id" /> 

414 <record instrument="patient_record" redcap_field="record_id" /> 

415 <instruments> 

416 <instrument name="bmi" task="bmi"> 

417 <fields> 

418 <field /> 

419 </fields> 

420 </instrument> 

421 </instruments> 

422 </fieldmap> 

423 """) # noqa: E501 

424 fieldmap_file.flush() 

425 

426 with self.assertRaises(RedcapExportException) as cm: 

427 RedcapFieldmap(fieldmap_file.name) 

428 

429 message = str(cm.exception) 

430 self.assertIn( 

431 "'field' must have attributes: name, formula", 

432 message 

433 ) 

434 self.assertIn(fieldmap_file.name, message) 

435 

436 

437# ============================================================================= 

438# Integration testing 

439# ============================================================================= 

440 

441class RedcapExportTestCase(DemoDatabaseTestCase): 

442 fieldmap = "" 

443 

444 def setUp(self) -> None: 

445 recipientinfo = ExportRecipientInfo() 

446 

447 self.recipient = ExportRecipient(recipientinfo) 

448 self.recipient.primary_idnum = 1001 

449 

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

451 self.recipient.id = 1 

452 self.recipient.recipient_name = "test" 

453 self.recipient.redcap_fieldmap_filename = os.path.join( 

454 self.tmpdir_obj.name, "redcap_fieldmap.xml" 

455 ) 

456 self.write_fieldmaps(self.recipient.redcap_fieldmap_filename) 

457 

458 super().setUp() 

459 

460 def write_fieldmaps(self, filename: str) -> None: 

461 with open(filename, "w") as f: 

462 f.write(self.fieldmap) 

463 

464 def create_patient_with_idnum_1001(self) -> "Patient": 

465 from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition 

466 from camcops_server.cc_modules.cc_patient import Patient 

467 from camcops_server.cc_modules.cc_patientidnum import PatientIdNum 

468 

469 patient = Patient() 

470 patient.id = 2 

471 self._apply_standard_db_fields(patient) 

472 patient.forename = "Forename2" 

473 patient.surname = "Surname2" 

474 patient.dob = pendulum.parse("1975-12-12") 

475 self.dbsession.add(patient) 

476 

477 idnumdef_1001 = IdNumDefinition() 

478 idnumdef_1001.which_idnum = 1001 

479 idnumdef_1001.description = "Test idnumdef 1001" 

480 self.dbsession.add(idnumdef_1001) 

481 self.dbsession.commit() 

482 

483 patient_idnum1 = PatientIdNum() 

484 patient_idnum1.id = 3 

485 self._apply_standard_db_fields(patient_idnum1) 

486 patient_idnum1.patient_id = patient.id 

487 patient_idnum1.which_idnum = 1001 

488 patient_idnum1.idnum_value = 555 

489 self.dbsession.add(patient_idnum1) 

490 self.dbsession.commit() 

491 

492 return patient 

493 

494 

495class BmiRedcapExportTestCase(RedcapExportTestCase): 

496 def __init__(self, *args, **kwargs) -> None: 

497 super().__init__(*args, **kwargs) 

498 self.id_sequence = self.get_id() 

499 

500 @staticmethod 

501 def get_id() -> Generator[int, None, None]: 

502 i = 1 

503 

504 while True: 

505 yield i 

506 i += 1 

507 

508 

509class BmiRedcapValidFieldmapTestCase(BmiRedcapExportTestCase): 

510 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

511<fieldmap> 

512 <patient instrument="patient_record" redcap_field="patient_id" /> 

513 <record instrument="instrument_with_record_id" redcap_field="record_id" /> 

514 <instruments> 

515 <instrument task="bmi" name="bmi"> 

516 <fields> 

517 <field name="pa_height" formula="format(task.height_m, '.1f')" /> 

518 <field name="pa_weight" formula="format(task.mass_kg, '.1f')" /> 

519 <field name="bmi_date" formula="format_datetime(task.when_created, DateFormat.ISO8601_DATE_ONLY)" /> 

520 </fields> 

521 </instrument> 

522 </instruments> 

523</fieldmap>""" # noqa: E501 

524 

525 

526class BmiRedcapExportTests(BmiRedcapValidFieldmapTestCase): 

527 """ 

528 These are more of a test of the fieldmap code than anything 

529 related to the BMI task 

530 """ 

531 

532 def create_tasks(self) -> None: 

533 from camcops_server.tasks.bmi import Bmi 

534 

535 patient = self.create_patient_with_idnum_1001() 

536 self.task = Bmi() 

537 self.apply_standard_task_fields(self.task) 

538 self.task.id = next(self.id_sequence) 

539 self.task.height_m = 1.83 

540 self.task.mass_kg = 67.57 

541 self.task.patient_id = patient.id 

542 self.dbsession.add(self.task) 

543 self.dbsession.commit() 

544 

545 def test_record_exported(self) -> None: 

546 from camcops_server.cc_modules.cc_exportmodels import ( 

547 ExportedTask, 

548 ExportedTaskRedcap 

549 ) 

550 

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

552 exported_task_redcap = ExportedTaskRedcap(exported_task) 

553 

554 exporter = MockRedcapTaskExporter() 

555 project = exporter.get_project() 

556 project.export_records.return_value = DataFrame({"patient_id": []}) 

557 project.import_records.return_value = ["123,0"] 

558 project.export_project_info.return_value = { 

559 "record_autonumbering_enabled": 1 

560 } 

561 

562 exporter.export_task(self.req, exported_task_redcap) 

563 self.assertEquals(exported_task_redcap.redcap_record_id, "123") 

564 self.assertEquals(exported_task_redcap.redcap_instrument_name, "bmi") 

565 self.assertEquals(exported_task_redcap.redcap_instance_id, 1) 

566 

567 args, kwargs = project.export_records.call_args 

568 

569 self.assertIn("bmi", kwargs['forms']) 

570 self.assertIn("patient_record", kwargs['forms']) 

571 self.assertIn("instrument_with_record_id", kwargs['forms']) 

572 

573 # Initial call with original record 

574 args, kwargs = project.import_records.call_args_list[0] 

575 

576 rows = args[0] 

577 record = rows[0] 

578 

579 self.assertEquals(record["redcap_repeat_instrument"], "bmi") 

580 self.assertEquals(record["redcap_repeat_instance"], 1) 

581 self.assertEquals(record["record_id"], "0") 

582 self.assertEquals(record["bmi_complete"], 

583 RedcapRecordStatus.COMPLETE.value) 

584 self.assertEquals(record["bmi_date"], "2010-07-07") 

585 

586 self.assertEquals(record["pa_height"], "1.8") 

587 self.assertEquals(record["pa_weight"], "67.6") 

588 

589 self.assertEquals(kwargs["return_content"], "auto_ids") 

590 self.assertTrue(kwargs["force_auto_number"]) 

591 

592 # Second call with updated patient ID 

593 args, kwargs = project.import_records.call_args_list[1] 

594 rows = args[0] 

595 record = rows[0] 

596 

597 self.assertEquals(record["patient_id"], 555) 

598 

599 def test_record_exported_with_non_integer_id(self) -> None: 

600 from camcops_server.cc_modules.cc_exportmodels import ( 

601 ExportedTask, 

602 ExportedTaskRedcap 

603 ) 

604 

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

606 exported_task_redcap = ExportedTaskRedcap(exported_task) 

607 

608 exporter = MockRedcapTaskExporter() 

609 project = exporter.get_project() 

610 project.export_records.return_value = DataFrame({"patient_id": []}) 

611 project.import_records.return_value = ["15-123,0"] 

612 project.export_project_info.return_value = { 

613 "record_autonumbering_enabled": 1 

614 } 

615 

616 exporter.export_task(self.req, exported_task_redcap) 

617 self.assertEquals(exported_task_redcap.redcap_record_id, "15-123") 

618 

619 def test_record_id_generated_when_no_autonumbering(self) -> None: 

620 from camcops_server.cc_modules.cc_exportmodels import ( 

621 ExportedTask, 

622 ExportedTaskRedcap 

623 ) 

624 

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

626 exported_task_redcap = ExportedTaskRedcap(exported_task) 

627 

628 exporter = MockRedcapTaskExporter() 

629 project = exporter.get_project() 

630 project.export_records.return_value = DataFrame({"patient_id": []}) 

631 project.import_records.return_value = {"count": 1} 

632 project.export_project_info.return_value = { 

633 "record_autonumbering_enabled": 0 

634 } 

635 project.generate_next_record_name.return_value = "15-29" 

636 

637 exporter.export_task(self.req, exported_task_redcap) 

638 

639 # Initial call with original record 

640 args, kwargs = project.import_records.call_args_list[0] 

641 

642 rows = args[0] 

643 record = rows[0] 

644 

645 self.assertEquals(record["record_id"], "15-29") 

646 self.assertEquals(kwargs["return_content"], "count") 

647 self.assertFalse(kwargs["force_auto_number"]) 

648 

649 def test_record_imported_when_no_existing_records(self) -> None: 

650 from camcops_server.cc_modules.cc_exportmodels import ( 

651 ExportedTask, 

652 ExportedTaskRedcap, 

653 ) 

654 

655 exporter = MockRedcapTaskExporter() 

656 project = exporter.get_project() 

657 project.export_records.return_value = DataFrame() 

658 project.import_records.return_value = ["1,0"] 

659 project.export_project_info.return_value = { 

660 "record_autonumbering_enabled": 1 

661 } 

662 

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

664 exported_task_redcap = ExportedTaskRedcap(exported_task) 

665 exporter.export_task(self.req, exported_task_redcap) 

666 

667 self.assertEquals(exported_task_redcap.redcap_record_id, "1") 

668 self.assertEquals(exported_task_redcap.redcap_instrument_name, "bmi") 

669 self.assertEquals(exported_task_redcap.redcap_instance_id, 1) 

670 

671 

672class BmiRedcapUpdateTests(BmiRedcapValidFieldmapTestCase): 

673 def create_tasks(self) -> None: 

674 from camcops_server.tasks.bmi import Bmi 

675 patient = self.create_patient_with_idnum_1001() 

676 self.task1 = Bmi() 

677 self.apply_standard_task_fields(self.task1) 

678 self.task1.id = next(self.id_sequence) 

679 self.task1.height_m = 1.83 

680 self.task1.mass_kg = 67.57 

681 self.task1.patient_id = patient.id 

682 self.dbsession.add(self.task1) 

683 

684 self.task2 = Bmi() 

685 self.apply_standard_task_fields(self.task2) 

686 self.task2.id = next(self.id_sequence) 

687 self.task2.height_m = 1.83 

688 self.task2.mass_kg = 68.5 

689 self.task2.patient_id = patient.id 

690 self.dbsession.add(self.task2) 

691 self.dbsession.commit() 

692 

693 def test_existing_record_id_used_for_update(self) -> None: 

694 from camcops_server.cc_modules.cc_exportmodels import ( 

695 ExportedTask, 

696 ExportedTaskRedcap, 

697 ) 

698 

699 exporter = MockRedcapTaskExporter() 

700 project = exporter.get_project() 

701 project.export_records.return_value = DataFrame({"patient_id": []}) 

702 project.import_records.return_value = ["123,0"] 

703 project.export_project_info.return_value = { 

704 "record_autonumbering_enabled": 1 

705 } 

706 

707 exported_task1 = ExportedTask(task=self.task1, recipient=self.recipient) 

708 exported_task_redcap1 = ExportedTaskRedcap(exported_task1) 

709 exporter.export_task(self.req, exported_task_redcap1) 

710 self.assertEquals(exported_task_redcap1.redcap_record_id, "123") 

711 self.assertEquals(exported_task_redcap1.redcap_instrument_name, "bmi") 

712 self.assertEquals(exported_task_redcap1.redcap_instance_id, 1) 

713 

714 project.export_records.return_value = DataFrame({ 

715 "record_id": ["123"], 

716 "patient_id": [555], 

717 "redcap_repeat_instrument": ["bmi"], 

718 "redcap_repeat_instance": [1], 

719 }) 

720 exported_task2 = ExportedTask(task=self.task2, recipient=self.recipient) 

721 exported_task_redcap2 = ExportedTaskRedcap(exported_task2) 

722 

723 exporter.export_task(self.req, exported_task_redcap2) 

724 self.assertEquals(exported_task_redcap2.redcap_record_id, "123") 

725 self.assertEquals(exported_task_redcap2.redcap_instrument_name, "bmi") 

726 self.assertEquals(exported_task_redcap2.redcap_instance_id, 2) 

727 

728 # Third call (after initial record and patient ID) 

729 args, kwargs = project.import_records.call_args_list[2] 

730 

731 rows = args[0] 

732 record = rows[0] 

733 

734 self.assertEquals(record["record_id"], "123") 

735 self.assertEquals(record["redcap_repeat_instance"], 2) 

736 self.assertEquals(kwargs["return_content"], "count") 

737 self.assertFalse(kwargs["force_auto_number"]) 

738 

739 

740class Phq9RedcapExportTests(RedcapExportTestCase): 

741 """ 

742 These are more of a test of the fieldmap code than anything 

743 related to the PHQ9 task. For these we have also renamed the record_id 

744 field. 

745 """ 

746 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

747<fieldmap> 

748 <patient instrument="patient_record" redcap_field="patient_id" /> 

749 <record instrument="patient_record" redcap_field="my_record_id" /> 

750 <instruments> 

751 <instrument task="phq9" name="patient_health_questionnaire_9"> 

752 <fields> 

753 <field name="phq9_how_difficult" formula="task.q10 + 1 if task.q10 is not None else None" /> 

754 <field name="phq9_total_score" formula="task.total_score()" /> 

755 <field name="phq9_first_name" formula="task.patient.forename" /> 

756 <field name="phq9_last_name" formula="task.patient.surname" /> 

757 <field name="phq9_date_enrolled" formula="format_datetime(task.when_created,DateFormat.ISO8601_DATE_ONLY)" /> 

758 <field name="phq9_1" formula="task.q1" /> 

759 <field name="phq9_2" formula="task.q2" /> 

760 <field name="phq9_3" formula="task.q3" /> 

761 <field name="phq9_4" formula="task.q4" /> 

762 <field name="phq9_5" formula="task.q5" /> 

763 <field name="phq9_6" formula="task.q6" /> 

764 <field name="phq9_7" formula="task.q7" /> 

765 <field name="phq9_8" formula="task.q8" /> 

766 <field name="phq9_9" formula="task.q9" /> 

767 </fields> 

768 </instrument> 

769 </instruments> 

770</fieldmap>""" # noqa: E501 

771 

772 def __init__(self, *args, **kwargs) -> None: 

773 super().__init__(*args, **kwargs) 

774 self.id_sequence = self.get_id() 

775 

776 @staticmethod 

777 def get_id() -> Generator[int, None, None]: 

778 i = 1 

779 

780 while True: 

781 yield i 

782 i += 1 

783 

784 def create_tasks(self) -> None: 

785 from camcops_server.tasks.phq9 import Phq9 

786 patient = self.create_patient_with_idnum_1001() 

787 self.task = Phq9() 

788 self.apply_standard_task_fields(self.task) 

789 self.task.id = next(self.id_sequence) 

790 self.task.q1 = 0 

791 self.task.q2 = 1 

792 self.task.q3 = 2 

793 self.task.q4 = 3 

794 self.task.q5 = 0 

795 self.task.q6 = 1 

796 self.task.q7 = 2 

797 self.task.q8 = 3 

798 self.task.q9 = 0 

799 self.task.q10 = 3 

800 self.task.patient_id = patient.id 

801 self.dbsession.add(self.task) 

802 self.dbsession.commit() 

803 

804 def test_record_exported(self) -> None: 

805 from camcops_server.cc_modules.cc_exportmodels import ( 

806 ExportedTask, 

807 ExportedTaskRedcap, 

808 ) 

809 

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

811 exported_task_redcap = ExportedTaskRedcap(exported_task) 

812 

813 exporter = MockRedcapTaskExporter() 

814 project = exporter.get_project() 

815 project.export_records.return_value = DataFrame({"patient_id": []}) 

816 project.import_records.return_value = ["123,0"] 

817 project.export_project_info.return_value = { 

818 "record_autonumbering_enabled": 1 

819 } 

820 

821 exporter.export_task(self.req, exported_task_redcap) 

822 self.assertEquals(exported_task_redcap.redcap_record_id, "123") 

823 self.assertEquals(exported_task_redcap.redcap_instrument_name, 

824 "patient_health_questionnaire_9") 

825 self.assertEquals(exported_task_redcap.redcap_instance_id, 1) 

826 

827 # Initial call with new record 

828 args, kwargs = project.import_records.call_args_list[0] 

829 

830 rows = args[0] 

831 record = rows[0] 

832 

833 self.assertEquals(record["redcap_repeat_instrument"], 

834 "patient_health_questionnaire_9") 

835 self.assertEquals(record["my_record_id"], "0") 

836 self.assertEquals(record["patient_health_questionnaire_9_complete"], 

837 RedcapRecordStatus.COMPLETE.value) 

838 self.assertEquals(record["phq9_how_difficult"], 4) 

839 self.assertEquals(record["phq9_total_score"], 12) 

840 self.assertEquals(record["phq9_first_name"], "Forename2") 

841 self.assertEquals(record["phq9_last_name"], "Surname2") 

842 self.assertEquals(record["phq9_date_enrolled"], "2010-07-07") 

843 

844 self.assertEquals(record["phq9_1"], 0) 

845 self.assertEquals(record["phq9_2"], 1) 

846 self.assertEquals(record["phq9_3"], 2) 

847 self.assertEquals(record["phq9_4"], 3) 

848 self.assertEquals(record["phq9_5"], 0) 

849 self.assertEquals(record["phq9_6"], 1) 

850 self.assertEquals(record["phq9_7"], 2) 

851 self.assertEquals(record["phq9_8"], 3) 

852 self.assertEquals(record["phq9_9"], 0) 

853 

854 self.assertEquals(kwargs["return_content"], "auto_ids") 

855 self.assertTrue(kwargs["force_auto_number"]) 

856 

857 # Second call with patient ID 

858 args, kwargs = project.import_records.call_args_list[1] 

859 

860 rows = args[0] 

861 record = rows[0] 

862 self.assertEquals(record["patient_id"], 555) 

863 

864 

865class MedicationTherapyRedcapExportTests(RedcapExportTestCase): 

866 """ 

867 These are more of a test of the file upload code than anything 

868 related to the KhandakerMojoMedicationTherapy task 

869 """ 

870 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

871<fieldmap> 

872 <event name="event_1_arm_1" /> 

873 <patient instrument="patient_record" redcap_field="patient_id" /> 

874 <record instrument="patient_record" redcap_field="record_id" /> 

875 <instruments> 

876 <instrument task="khandaker_mojo_medicationtherapy" name="medication_table"> 

877 <files> 

878 <field name="medtbl_medication_items" formula="task.get_pdf(request)" /> 

879 </files> 

880 </instrument> 

881 </instruments> 

882</fieldmap>""" # noqa: E501 

883 

884 def __init__(self, *args, **kwargs) -> None: 

885 super().__init__(*args, **kwargs) 

886 self.id_sequence = self.get_id() 

887 

888 @staticmethod 

889 def get_id() -> Generator[int, None, None]: 

890 i = 1 

891 

892 while True: 

893 yield i 

894 i += 1 

895 

896 def create_tasks(self) -> None: 

897 from camcops_server.tasks.khandaker_mojo_medicationtherapy import ( 

898 KhandakerMojoMedicationTherapy, 

899 ) 

900 

901 patient = self.create_patient_with_idnum_1001() 

902 self.task = KhandakerMojoMedicationTherapy() 

903 self.apply_standard_task_fields(self.task) 

904 self.task.id = next(self.id_sequence) 

905 self.task.patient_id = patient.id 

906 self.dbsession.add(self.task) 

907 self.dbsession.commit() 

908 

909 def test_record_exported(self) -> None: 

910 from camcops_server.cc_modules.cc_exportmodels import ( 

911 ExportedTask, 

912 ExportedTaskRedcap 

913 ) 

914 

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

916 exported_task_redcap = ExportedTaskRedcap(exported_task) 

917 

918 exporter = MockRedcapTaskExporter() 

919 project = exporter.get_project() 

920 project.export_records.return_value = DataFrame({"patient_id": []}) 

921 project.import_records.return_value = ["123,0"] 

922 project.export_project_info.return_value = { 

923 "record_autonumbering_enabled": 1 

924 } 

925 

926 # We can't just look at the call_args on the mock object because 

927 # the file will already have been closed by then 

928 # noinspection PyUnusedLocal 

929 def read_pdf_bytes(*import_file_args, **import_file_kwargs) -> None: 

930 # record, field, fname, fobj 

931 file_obj = import_file_args[3] 

932 read_pdf_bytes.pdf_header = file_obj.read(5) 

933 

934 project.import_file.side_effect = read_pdf_bytes 

935 

936 exporter.export_task(self.req, exported_task_redcap) 

937 self.assertEquals(exported_task_redcap.redcap_record_id, "123") 

938 self.assertEquals(exported_task_redcap.redcap_instrument_name, 

939 "medication_table") 

940 self.assertEquals(exported_task_redcap.redcap_instance_id, 1) 

941 

942 args, kwargs = project.import_file.call_args 

943 

944 record_id = args[0] 

945 fieldname = args[1] 

946 filename = args[2] 

947 

948 self.assertEquals(record_id, "123") 

949 self.assertEquals(fieldname, "medtbl_medication_items") 

950 self.assertEquals( 

951 filename, 

952 "khandaker_mojo_medicationtherapy_123_medtbl_medication_items" 

953 ) 

954 

955 self.assertEquals(kwargs["repeat_instance"], 1) 

956 # noinspection PyUnresolvedReferences 

957 self.assertEquals(read_pdf_bytes.pdf_header, b"%PDF-") 

958 self.assertEquals(kwargs["event"], "event_1_arm_1") 

959 

960 

961class MultipleTaskRedcapExportTests(RedcapExportTestCase): 

962 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

963<fieldmap> 

964 <patient instrument="patient_record" redcap_field="patient_id" /> 

965 <record instrument="patient_record" redcap_field="record_id" /> 

966 <instruments> 

967 <instrument task="bmi" name="bmi" event="bmi_event"> 

968 <fields> 

969 <field name="pa_height" formula="format(task.height_m, '.1f')" /> 

970 <field name="pa_weight" formula="format(task.mass_kg, '.1f')" /> 

971 <field name="bmi_date" formula="format_datetime(task.when_created, DateFormat.ISO8601_DATE_ONLY)" /> 

972 </fields> 

973 </instrument> 

974 <instrument task="khandaker_mojo_medicationtherapy" name="medication_table" event="mojo_event"> 

975 <files> 

976 <field name="medtbl_medication_items" formula="task.get_pdf(request)" /> 

977 </files> 

978 </instrument> 

979 </instruments> 

980</fieldmap> 

981""" # noqa: E501 

982 

983 def __init__(self, *args, **kwargs) -> None: 

984 super().__init__(*args, **kwargs) 

985 self.id_sequence = self.get_id() 

986 

987 @staticmethod 

988 def get_id() -> Generator[int, None, None]: 

989 i = 1 

990 

991 while True: 

992 yield i 

993 i += 1 

994 

995 def create_tasks(self) -> None: 

996 from camcops_server.tasks.khandaker_mojo_medicationtherapy import ( 

997 KhandakerMojoMedicationTherapy, 

998 ) 

999 

1000 patient = self.create_patient_with_idnum_1001() 

1001 self.mojo_task = KhandakerMojoMedicationTherapy() 

1002 self.apply_standard_task_fields(self.mojo_task) 

1003 self.mojo_task.id = next(self.id_sequence) 

1004 self.mojo_task.patient_id = patient.id 

1005 self.dbsession.add(self.mojo_task) 

1006 self.dbsession.commit() 

1007 

1008 from camcops_server.tasks.bmi import Bmi 

1009 self.bmi_task = Bmi() 

1010 self.apply_standard_task_fields(self.bmi_task) 

1011 self.bmi_task.id = next(self.id_sequence) 

1012 self.bmi_task.height_m = 1.83 

1013 self.bmi_task.mass_kg = 67.57 

1014 self.bmi_task.patient_id = patient.id 

1015 self.dbsession.add(self.bmi_task) 

1016 self.dbsession.commit() 

1017 

1018 def test_instance_ids_on_different_tasks_in_same_record(self) -> None: 

1019 from camcops_server.cc_modules.cc_exportmodels import ( 

1020 ExportedTask, 

1021 ExportedTaskRedcap, 

1022 ) 

1023 exporter = MockRedcapTaskExporter() 

1024 project = exporter.get_project() 

1025 project.export_records.return_value = DataFrame({"patient_id": []}) 

1026 project.import_records.return_value = ["123,0"] 

1027 project.export_project_info.return_value = { 

1028 "record_autonumbering_enabled": 1 

1029 } 

1030 

1031 exported_task_mojo = ExportedTask(task=self.mojo_task, 

1032 recipient=self.recipient) 

1033 exported_task_redcap_mojo = ExportedTaskRedcap(exported_task_mojo) 

1034 exporter.export_task(self.req, exported_task_redcap_mojo) 

1035 self.assertEquals(exported_task_redcap_mojo.redcap_record_id, "123") 

1036 args, kwargs = project.import_file.call_args 

1037 

1038 self.assertEquals(kwargs["repeat_instance"], 1) 

1039 

1040 project.export_records.return_value = DataFrame({ 

1041 "record_id": ["123"], 

1042 "patient_id": [555], 

1043 "redcap_repeat_instrument": ["khandaker_mojo_medicationtherapy"], 

1044 "redcap_repeat_instance": [1], 

1045 }) 

1046 exported_task_bmi = ExportedTask(task=self.bmi_task, 

1047 recipient=self.recipient) 

1048 exported_task_redcap_bmi = ExportedTaskRedcap(exported_task_bmi) 

1049 

1050 exporter.export_task(self.req, exported_task_redcap_bmi) 

1051 

1052 # Import of second task, but is first instance 

1053 # (third call to import_records) 

1054 args, kwargs = project.import_records.call_args_list[2] 

1055 

1056 rows = args[0] 

1057 record = rows[0] 

1058 

1059 self.assertEquals(record["redcap_repeat_instance"], 1) 

1060 

1061 def test_imported_into_different_events(self) -> None: 

1062 from camcops_server.cc_modules.cc_exportmodels import ( 

1063 ExportedTask, 

1064 ExportedTaskRedcap, 

1065 ) 

1066 exporter = MockRedcapTaskExporter() 

1067 project = exporter.get_project() 

1068 

1069 project.is_longitudinal = mock.Mock(return_value=True) 

1070 project.export_records.return_value = DataFrame({"patient_id": []}) 

1071 project.import_records.return_value = ["123,0"] 

1072 project.export_project_info.return_value = { 

1073 "record_autonumbering_enabled": 1 

1074 } 

1075 

1076 exported_task_mojo = ExportedTask(task=self.mojo_task, 

1077 recipient=self.recipient) 

1078 exported_task_redcap_mojo = ExportedTaskRedcap(exported_task_mojo) 

1079 

1080 exporter.export_task(self.req, exported_task_redcap_mojo) 

1081 

1082 args, kwargs = project.import_records.call_args_list[0] 

1083 rows = args[0] 

1084 record = rows[0] 

1085 

1086 self.assertEquals(record["redcap_event_name"], "mojo_event") 

1087 args, kwargs = project.import_file.call_args 

1088 

1089 self.assertEquals(kwargs["event"], "mojo_event") 

1090 

1091 exported_task_bmi = ExportedTask(task=self.bmi_task, 

1092 recipient=self.recipient) 

1093 exported_task_redcap_bmi = ExportedTaskRedcap(exported_task_bmi) 

1094 

1095 exporter.export_task(self.req, exported_task_redcap_bmi) 

1096 

1097 # Import of second task (third call to import_records) 

1098 args, kwargs = project.import_records.call_args_list[2] 

1099 rows = args[0] 

1100 record = rows[0] 

1101 self.assertEquals(record["redcap_event_name"], "bmi_event") 

1102 

1103 

1104class BadConfigurationRedcapTests(RedcapExportTestCase): 

1105 def __init__(self, *args, **kwargs) -> None: 

1106 super().__init__(*args, **kwargs) 

1107 self.id_sequence = self.get_id() 

1108 

1109 @staticmethod 

1110 def get_id() -> Generator[int, None, None]: 

1111 i = 1 

1112 

1113 while True: 

1114 yield i 

1115 i += 1 

1116 

1117 def create_tasks(self) -> None: 

1118 from camcops_server.tasks.bmi import Bmi 

1119 patient = self.create_patient_with_idnum_1001() 

1120 self.task = Bmi() 

1121 self.apply_standard_task_fields(self.task) 

1122 self.task.id = next(self.id_sequence) 

1123 self.task.height_m = 1.83 

1124 self.task.mass_kg = 67.57 

1125 self.task.patient_id = patient.id 

1126 self.dbsession.add(self.task) 

1127 self.dbsession.commit() 

1128 

1129 

1130class MissingInstrumentRedcapTests(BadConfigurationRedcapTests): 

1131 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

1132<fieldmap> 

1133 <patient instrument="patient_record" redcap_field="patient_id" /> 

1134 <record instrument="patient_record" redcap_field="record_id" /> 

1135 <instruments> 

1136 <instrument task="phq9" name="patient_health_questionnaire_9"> 

1137 <fields> 

1138 </fields> 

1139 </instrument> 

1140 </instruments> 

1141</fieldmap>""" # noqa: E501 

1142 

1143 def test_raises_when_instrument_missing_from_fieldmap(self) -> None: 

1144 from camcops_server.cc_modules.cc_exportmodels import ( 

1145 ExportedTask, 

1146 ExportedTaskRedcap 

1147 ) 

1148 

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

1150 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1151 

1152 exporter = MockRedcapTaskExporter() 

1153 project = exporter.get_project() 

1154 project.export_records.return_value = DataFrame({"patient_id": []}) 

1155 project.import_records.return_value = ["123,0"] 

1156 

1157 with self.assertRaises(RedcapExportException) as cm: 

1158 exporter.export_task(self.req, exported_task_redcap) 

1159 

1160 message = str(cm.exception) 

1161 self.assertIn("Instrument for task 'bmi' is missing from the fieldmap", 

1162 message) 

1163 

1164 

1165class IncorrectRecordIdRedcapTests(BadConfigurationRedcapTests): 

1166 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

1167<fieldmap> 

1168 <patient instrument="patient_record" redcap_field="patient_id" /> 

1169 <record instrument="patient_record" redcap_field="my_record_id" /> 

1170 <instruments> 

1171 <instrument task="bmi" name="bmi"> 

1172 <fields> 

1173 </fields> 

1174 </instrument> 

1175 </instruments> 

1176</fieldmap>""" # noqa: E501 

1177 

1178 def test_raises_when_record_id_is_incorrect(self) -> None: 

1179 from camcops_server.cc_modules.cc_exportmodels import ( 

1180 ExportedTask, 

1181 ExportedTaskRedcap 

1182 ) 

1183 

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

1185 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1186 

1187 exporter = MockRedcapTaskExporter() 

1188 project = exporter.get_project() 

1189 project.export_records.return_value = DataFrame({ 

1190 "record_id": ["123"], 

1191 "patient_id": [555], 

1192 "redcap_repeat_instrument": ["bmi"], 

1193 "redcap_repeat_instance": [1], 

1194 }) 

1195 project.import_records.return_value = ["123,0"] 

1196 project.export_project_info.return_value = { 

1197 "record_autonumbering_enabled": 1 

1198 } 

1199 

1200 with self.assertRaises(RedcapExportException) as cm: 

1201 exporter.export_task(self.req, exported_task_redcap) 

1202 

1203 message = str(cm.exception) 

1204 self.assertIn("Field 'my_record_id' does not exist in REDCap", 

1205 message) 

1206 

1207 

1208class IncorrectPatientIdRedcapTests(BadConfigurationRedcapTests): 

1209 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

1210<fieldmap> 

1211 <patient instrument="patient_record" redcap_field="my_patient_id" /> 

1212 <record instrument="patient_record" redcap_field="record_id" /> 

1213 <instruments> 

1214 <instrument task="bmi" name="bmi"> 

1215 <fields> 

1216 </fields> 

1217 </instrument> 

1218 </instruments> 

1219</fieldmap>""" # noqa: E501 

1220 

1221 def test_raises_when_patient_id_is_incorrect(self) -> None: 

1222 from camcops_server.cc_modules.cc_exportmodels import ( 

1223 ExportedTask, 

1224 ExportedTaskRedcap 

1225 ) 

1226 

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

1228 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1229 

1230 exporter = MockRedcapTaskExporter() 

1231 project = exporter.get_project() 

1232 project.export_records.return_value = DataFrame({ 

1233 "record_id": ["123"], 

1234 "patient_id": [555], 

1235 "redcap_repeat_instrument": ["bmi"], 

1236 "redcap_repeat_instance": [1], 

1237 }) 

1238 project.import_records.return_value = ["123,0"] 

1239 project.export_project_info.return_value = { 

1240 "record_autonumbering_enabled": 1 

1241 } 

1242 

1243 with self.assertRaises(RedcapExportException) as cm: 

1244 exporter.export_task(self.req, exported_task_redcap) 

1245 

1246 message = str(cm.exception) 

1247 self.assertIn("Field 'my_patient_id' does not exist in REDCap", 

1248 message) 

1249 

1250 

1251class MissingPatientInstrumentRedcapTests(BadConfigurationRedcapTests): 

1252 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

1253<fieldmap> 

1254 <patient instrument="patient_record" redcap_field="my_patient_id" /> 

1255 <record instrument="patient_record" redcap_field="record_id" /> 

1256 <instruments> 

1257 <instrument task="bmi" name="bmi"> 

1258 <fields> 

1259 </fields> 

1260 </instrument> 

1261 </instruments> 

1262</fieldmap>""" # noqa: E501 

1263 

1264 def test_raises_when_instrument_is_missing(self) -> None: 

1265 from camcops_server.cc_modules.cc_exportmodels import ( 

1266 ExportedTask, 

1267 ExportedTaskRedcap 

1268 ) 

1269 

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

1271 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1272 

1273 exporter = MockRedcapTaskExporter() 

1274 project = exporter.get_project() 

1275 project.export_records.side_effect = redcap.RedcapError( 

1276 "Something went wrong" 

1277 ) 

1278 

1279 with self.assertRaises(RedcapExportException) as cm: 

1280 exporter.export_task(self.req, exported_task_redcap) 

1281 

1282 message = str(cm.exception) 

1283 self.assertIn("Something went wrong", message) 

1284 

1285 

1286class MissingEventRedcapTests(BadConfigurationRedcapTests): 

1287 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

1288<fieldmap> 

1289 <patient instrument="patient_record" redcap_field="my_patient_id" /> 

1290 <record instrument="patient_record" redcap_field="record_id" /> 

1291 <instruments> 

1292 <instrument task="bmi" name="bmi"> 

1293 <fields> 

1294 </fields> 

1295 </instrument> 

1296 </instruments> 

1297</fieldmap>""" # noqa: E501 

1298 

1299 def test_raises_for_longitudinal_project(self) -> None: 

1300 from camcops_server.cc_modules.cc_exportmodels import ( 

1301 ExportedTask, 

1302 ExportedTaskRedcap 

1303 ) 

1304 

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

1306 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1307 

1308 exporter = MockRedcapTaskExporter() 

1309 project = exporter.get_project() 

1310 

1311 project.is_longitudinal = mock.Mock(return_value=True) 

1312 

1313 with self.assertRaises(RedcapExportException) as cm: 

1314 exporter.export_task(self.req, exported_task_redcap) 

1315 

1316 message = str(cm.exception) 

1317 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message) 

1318 

1319 

1320class MissingInstrumentEventRedcapTests(BadConfigurationRedcapTests): 

1321 fieldmap = """<?xml version="1.0" encoding="UTF-8"?> 

1322<fieldmap> 

1323 <patient instrument="patient_record" redcap_field="my_patient_id" /> 

1324 <record instrument="patient_record" redcap_field="record_id" /> 

1325 <instruments> 

1326 <instrument task="bmi" name="bmi"> 

1327 <fields> 

1328 </fields> 

1329 </instrument> 

1330 <instrument task="phq9" name="phq9" event="phq9_event"> 

1331 <fields> 

1332 </fields> 

1333 </instrument> 

1334 </instruments> 

1335</fieldmap>""" # noqa: E501 

1336 

1337 def test_raises_when_instrument_missing_event(self) -> None: 

1338 from camcops_server.cc_modules.cc_exportmodels import ( 

1339 ExportedTask, 

1340 ExportedTaskRedcap 

1341 ) 

1342 

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

1344 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1345 

1346 exporter = MockRedcapTaskExporter() 

1347 project = exporter.get_project() 

1348 

1349 project.is_longitudinal = mock.Mock(return_value=True) 

1350 

1351 with self.assertRaises(RedcapExportException) as cm: 

1352 exporter.export_task(self.req, exported_task_redcap) 

1353 

1354 message = str(cm.exception) 

1355 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message) 

1356 

1357 

1358class AnonymousTaskRedcapTests(RedcapExportTestCase): 

1359 def create_tasks(self) -> None: 

1360 from camcops_server.tasks.apeq_cpft_perinatal import APEQCPFTPerinatal 

1361 self.task = APEQCPFTPerinatal() 

1362 self.apply_standard_task_fields(self.task) 

1363 self.task.id = 1 

1364 self.dbsession.add(self.task) 

1365 self.dbsession.commit() 

1366 

1367 def test_raises_when_task_is_anonymous(self) -> None: 

1368 from camcops_server.cc_modules.cc_exportmodels import ( 

1369 ExportedTask, 

1370 ExportedTaskRedcap 

1371 ) 

1372 

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

1374 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1375 

1376 exporter = MockRedcapTaskExporter() 

1377 

1378 with self.assertRaises(RedcapExportException) as cm: 

1379 exporter.export_task(self.req, exported_task_redcap) 

1380 

1381 message = str(cm.exception) 

1382 self.assertIn("Skipping anonymous task 'apeq_cpft_perinatal'", message)