Coverage for cc_modules/tests/cc_redcap_tests.py: 16%

694 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_redcap_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""" 

27import os 

28import tempfile 

29from typing import Generator, TYPE_CHECKING 

30from unittest import mock, TestCase 

31 

32from pandas import DataFrame 

33import pendulum 

34import redcap 

35 

36from camcops_server.cc_modules.cc_constants import ConfigParamExportRecipient 

37from camcops_server.cc_modules.cc_exportrecipient import ExportRecipient 

38from camcops_server.cc_modules.cc_exportrecipientinfo import ( 

39 ExportRecipientInfo, 

40) 

41from camcops_server.cc_modules.cc_redcap import ( 

42 MISSING_EVENT_TAG_OR_ATTRIBUTE, 

43 RedcapExportException, 

44 RedcapFieldmap, 

45 RedcapNewRecordUploader, 

46 RedcapRecordStatus, 

47 RedcapTaskExporter, 

48) 

49from camcops_server.cc_modules.cc_unittest import BasicDatabaseTestCase 

50 

51if TYPE_CHECKING: 

52 from camcops_server.cc_modules.cc_patient import Patient 

53 

54 

55# ============================================================================= 

56# Unit testing 

57# ============================================================================= 

58 

59 

60class MockProject(mock.Mock): 

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

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

63 

64 self.export_project_info = mock.Mock() 

65 self.export_records = mock.Mock() 

66 self.generate_next_record_name = mock.Mock() 

67 self.import_file = mock.Mock() 

68 self.import_records = mock.Mock() 

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

70 

71 

72class MockRedcapTaskExporter(RedcapTaskExporter): 

73 def __init__(self) -> None: 

74 mock_project = MockProject() 

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

76 

77 config = mock.Mock() 

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

79 

80 

81class MockRedcapNewRecordUploader(RedcapNewRecordUploader): 

82 # noinspection PyMissingConstructor 

83 def __init__(self) -> None: 

84 self.req = mock.Mock() 

85 self.project = MockProject() 

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

87 

88 

89class RedcapExporterTests(TestCase): 

90 def test_next_instance_id_converted_to_int(self) -> None: 

91 import numpy 

92 

93 records = DataFrame( 

94 { 

95 "record_id": ["1", "1", "1", "1", "1"], 

96 "redcap_repeat_instrument": [ 

97 "bmi", 

98 "bmi", 

99 "bmi", 

100 "bmi", 

101 "bmi", 

102 ], 

103 "redcap_repeat_instance": [ 

104 numpy.float64(1.0), 

105 numpy.float64(2.0), 

106 numpy.float64(3.0), 

107 numpy.float64(4.0), 

108 numpy.float64(5.0), 

109 ], 

110 } 

111 ) 

112 

113 next_instance_id = RedcapTaskExporter._get_next_instance_id( 

114 records, "bmi", "record_id", "1" 

115 ) 

116 

117 self.assertEqual(next_instance_id, 6) 

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

119 

120 

121class RedcapExportErrorTests(TestCase): 

122 def test_raises_when_fieldmap_has_unknown_symbols(self) -> None: 

123 exporter = MockRedcapNewRecordUploader() 

124 

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

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

127 

128 field_dict = {} 

129 

130 with self.assertRaises(RedcapExportException) as cm: 

131 exporter.transform_fields(field_dict, task, fieldmap) 

132 

133 message = str(cm.exception) 

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

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

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

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

138 

139 def test_raises_when_fieldmap_empty_in_config(self) -> None: 

140 

141 exporter = MockRedcapTaskExporter() 

142 

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

144 with self.assertRaises(RedcapExportException) as cm: 

145 exporter.get_fieldmap_filename(recipient) 

146 

147 message = str(cm.exception) 

148 self.assertIn( 

149 f"{ConfigParamExportRecipient.REDCAP_FIELDMAP_FILENAME} " 

150 f"is empty in the config file", 

151 message, 

152 ) 

153 

154 def test_raises_when_fieldmap_not_set_in_config(self) -> None: 

155 

156 exporter = MockRedcapTaskExporter() 

157 

158 recipient = mock.Mock(redcap_fieldmap_filename=None) 

159 with self.assertRaises(RedcapExportException) as cm: 

160 exporter.get_fieldmap_filename(recipient) 

161 

162 message = str(cm.exception) 

163 self.assertIn( 

164 f"{ConfigParamExportRecipient.REDCAP_FIELDMAP_FILENAME} " 

165 f"is not set in the config file", 

166 message, 

167 ) 

168 

169 def test_raises_when_error_from_redcap_on_import(self) -> None: 

170 exporter = MockRedcapNewRecordUploader() 

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

172 "Something went wrong" 

173 ) 

174 

175 with self.assertRaises(RedcapExportException) as cm: 

176 record = {} 

177 exporter.upload_record(record) 

178 message = str(cm.exception) 

179 

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

181 

182 def test_raises_when_error_from_redcap_on_init(self) -> None: 

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

184 mock_init.side_effect = redcap.RedcapError("Something went wrong") 

185 

186 with self.assertRaises(RedcapExportException) as cm: 

187 exporter = RedcapTaskExporter() 

188 recipient = mock.Mock() 

189 exporter.get_project(recipient) 

190 

191 message = str(cm.exception) 

192 

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

194 

195 def test_raises_when_field_not_a_file_field(self) -> None: 

196 exporter = MockRedcapNewRecordUploader() 

197 exporter.project.import_file.side_effect = ValueError( 

198 "Error with file field" 

199 ) 

200 

201 task = mock.Mock() 

202 

203 with self.assertRaises(RedcapExportException) as cm: 

204 record_id = 1 

205 repeat_instance = 1 

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

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

208 message = str(cm.exception) 

209 

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

211 

212 def test_raises_when_error_from_redcap_on_import_file(self) -> None: 

213 exporter = MockRedcapNewRecordUploader() 

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

215 "Something went wrong" 

216 ) 

217 

218 task = mock.Mock() 

219 

220 with self.assertRaises(RedcapExportException) as cm: 

221 record_id = 1 

222 repeat_instance = 1 

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

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

225 message = str(cm.exception) 

226 

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

228 

229 

230class RedcapFieldmapTests(TestCase): 

231 def test_raises_when_xml_file_missing(self) -> None: 

232 with self.assertRaises(RedcapExportException) as cm: 

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

234 

235 message = str(cm.exception) 

236 

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

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

239 

240 def test_raises_when_fieldmap_missing(self) -> None: 

241 with tempfile.NamedTemporaryFile( 

242 mode="w", suffix="xml" 

243 ) as fieldmap_file: 

244 fieldmap_file.write( 

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

246<someothertag></someothertag> 

247""" 

248 ) 

249 fieldmap_file.flush() 

250 

251 with self.assertRaises(RedcapExportException) as cm: 

252 RedcapFieldmap(fieldmap_file.name) 

253 

254 message = str(cm.exception) 

255 self.assertIn( 

256 ( 

257 "Expected the root tag to be 'fieldmap' instead of " 

258 "'someothertag'" 

259 ), 

260 message, 

261 ) 

262 self.assertIn(fieldmap_file.name, message) 

263 

264 def test_raises_when_root_tag_missing(self) -> None: 

265 with tempfile.NamedTemporaryFile( 

266 mode="w", suffix="xml" 

267 ) as fieldmap_file: 

268 fieldmap_file.write( 

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

270""" 

271 ) 

272 fieldmap_file.flush() 

273 

274 with self.assertRaises(RedcapExportException) as cm: 

275 RedcapFieldmap(fieldmap_file.name) 

276 

277 message = str(cm.exception) 

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

279 self.assertIn(fieldmap_file.name, message) 

280 

281 def test_raises_when_patient_missing(self) -> None: 

282 with tempfile.NamedTemporaryFile( 

283 mode="w", suffix="xml" 

284 ) as fieldmap_file: 

285 fieldmap_file.write( 

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

287 <fieldmap> 

288 </fieldmap> 

289 """ 

290 ) 

291 fieldmap_file.flush() 

292 

293 with self.assertRaises(RedcapExportException) as cm: 

294 RedcapFieldmap(fieldmap_file.name) 

295 

296 message = str(cm.exception) 

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

298 self.assertIn(fieldmap_file.name, message) 

299 

300 def test_raises_when_patient_missing_attributes(self) -> None: 

301 with tempfile.NamedTemporaryFile( 

302 mode="w", suffix="xml" 

303 ) as fieldmap_file: 

304 fieldmap_file.write( 

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

306 <fieldmap> 

307 <patient /> 

308 </fieldmap> 

309 """ 

310 ) 

311 fieldmap_file.flush() 

312 

313 with self.assertRaises(RedcapExportException) as cm: 

314 RedcapFieldmap(fieldmap_file.name) 

315 

316 message = str(cm.exception) 

317 self.assertIn( 

318 "'patient' must have attributes: instrument, redcap_field", message 

319 ) 

320 self.assertIn(fieldmap_file.name, message) 

321 

322 def test_raises_when_record_missing(self) -> None: 

323 with tempfile.NamedTemporaryFile( 

324 mode="w", suffix="xml" 

325 ) as fieldmap_file: 

326 fieldmap_file.write( 

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

328 <fieldmap> 

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

330 </fieldmap> 

331 """ # noqa: E501 

332 ) 

333 fieldmap_file.flush() 

334 

335 with self.assertRaises(RedcapExportException) as cm: 

336 RedcapFieldmap(fieldmap_file.name) 

337 

338 message = str(cm.exception) 

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

340 self.assertIn(fieldmap_file.name, message) 

341 

342 def test_raises_when_record_missing_attributes(self) -> None: 

343 with tempfile.NamedTemporaryFile( 

344 mode="w", suffix="xml" 

345 ) as fieldmap_file: 

346 fieldmap_file.write( 

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

348 <fieldmap> 

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

350 <record /> 

351 </fieldmap> 

352 """ # noqa: E501 

353 ) 

354 fieldmap_file.flush() 

355 

356 with self.assertRaises(RedcapExportException) as cm: 

357 RedcapFieldmap(fieldmap_file.name) 

358 

359 message = str(cm.exception) 

360 self.assertIn( 

361 "'record' must have attributes: instrument, redcap_field", message 

362 ) 

363 self.assertIn(fieldmap_file.name, message) 

364 

365 def test_raises_when_instruments_missing(self) -> None: 

366 with tempfile.NamedTemporaryFile( 

367 mode="w", suffix="xml" 

368 ) as fieldmap_file: 

369 fieldmap_file.write( 

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

371 <fieldmap> 

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

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

374 </fieldmap> 

375 """ # noqa: E501 

376 ) 

377 fieldmap_file.flush() 

378 

379 with self.assertRaises(RedcapExportException) as cm: 

380 RedcapFieldmap(fieldmap_file.name) 

381 

382 message = str(cm.exception) 

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

384 self.assertIn(fieldmap_file.name, message) 

385 

386 def test_raises_when_instruments_missing_attributes(self) -> None: 

387 with tempfile.NamedTemporaryFile( 

388 mode="w", suffix="xml" 

389 ) as fieldmap_file: 

390 fieldmap_file.write( 

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

392 <fieldmap> 

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

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

395 <instruments> 

396 <instrument /> 

397 </instruments> 

398 </fieldmap> 

399 """ # noqa: E501 

400 ) 

401 fieldmap_file.flush() 

402 

403 with self.assertRaises(RedcapExportException) as cm: 

404 RedcapFieldmap(fieldmap_file.name) 

405 

406 message = str(cm.exception) 

407 self.assertIn("'instrument' must have attributes: name, task", message) 

408 self.assertIn(fieldmap_file.name, message) 

409 

410 def test_raises_when_file_fields_missing_attributes(self) -> None: 

411 with tempfile.NamedTemporaryFile( 

412 mode="w", suffix="xml" 

413 ) as fieldmap_file: 

414 fieldmap_file.write( 

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

416 <fieldmap> 

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

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

419 <instruments> 

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

421 <files> 

422 <field /> 

423 </files> 

424 </instrument> 

425 </instruments> 

426 </fieldmap> 

427 """ # noqa: E501 

428 ) 

429 fieldmap_file.flush() 

430 

431 with self.assertRaises(RedcapExportException) as cm: 

432 RedcapFieldmap(fieldmap_file.name) 

433 

434 message = str(cm.exception) 

435 self.assertIn("'field' must have attributes: name, formula", message) 

436 self.assertIn(fieldmap_file.name, message) 

437 

438 def test_raises_when_fields_missing_attributes(self) -> None: 

439 with tempfile.NamedTemporaryFile( 

440 mode="w", suffix="xml" 

441 ) as fieldmap_file: 

442 fieldmap_file.write( 

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

444 <fieldmap> 

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

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

447 <instruments> 

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

449 <fields> 

450 <field /> 

451 </fields> 

452 </instrument> 

453 </instruments> 

454 </fieldmap> 

455 """ # noqa: E501 

456 ) 

457 fieldmap_file.flush() 

458 

459 with self.assertRaises(RedcapExportException) as cm: 

460 RedcapFieldmap(fieldmap_file.name) 

461 

462 message = str(cm.exception) 

463 self.assertIn("'field' must have attributes: name, formula", message) 

464 self.assertIn(fieldmap_file.name, message) 

465 

466 

467# ============================================================================= 

468# Integration testing 

469# ============================================================================= 

470 

471 

472class RedcapExportTestCase(BasicDatabaseTestCase): 

473 fieldmap = "" 

474 

475 def setUp(self) -> None: 

476 recipientinfo = ExportRecipientInfo() 

477 

478 self.recipient = ExportRecipient(recipientinfo) 

479 self.recipient.primary_idnum = 1001 

480 

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

482 self.recipient.id = 1 

483 self.recipient.recipient_name = "test" 

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

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

486 ) 

487 self.write_fieldmaps(self.recipient.redcap_fieldmap_filename) 

488 

489 super().setUp() 

490 

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

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

493 f.write(self.fieldmap) 

494 

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

496 from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition 

497 from camcops_server.cc_modules.cc_patient import Patient 

498 from camcops_server.cc_modules.cc_patientidnum import PatientIdNum 

499 

500 patient = Patient() 

501 patient.id = 2 

502 self.apply_standard_db_fields(patient) 

503 patient.forename = "Forename2" 

504 patient.surname = "Surname2" 

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

506 self.dbsession.add(patient) 

507 

508 idnumdef_1001 = IdNumDefinition() 

509 idnumdef_1001.which_idnum = 1001 

510 idnumdef_1001.description = "Test idnumdef 1001" 

511 self.dbsession.add(idnumdef_1001) 

512 self.dbsession.commit() 

513 

514 patient_idnum1 = PatientIdNum() 

515 patient_idnum1.id = 3 

516 self.apply_standard_db_fields(patient_idnum1) 

517 patient_idnum1.patient_id = patient.id 

518 patient_idnum1.which_idnum = 1001 

519 patient_idnum1.idnum_value = 555 

520 self.dbsession.add(patient_idnum1) 

521 self.dbsession.commit() 

522 

523 return patient 

524 

525 

526class BmiRedcapExportTestCase(RedcapExportTestCase): 

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

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

529 self.id_sequence = self.get_id() 

530 

531 @staticmethod 

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

533 i = 1 

534 

535 while True: 

536 yield i 

537 i += 1 

538 

539 

540class BmiRedcapValidFieldmapTestCase(BmiRedcapExportTestCase): 

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

542<fieldmap> 

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

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

545 <instruments> 

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

547 <fields> 

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

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

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

551 </fields> 

552 </instrument> 

553 </instruments> 

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

555 

556 

557class BmiRedcapExportTests(BmiRedcapValidFieldmapTestCase): 

558 """ 

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

560 related to the BMI task 

561 """ 

562 

563 def create_tasks(self) -> None: 

564 from camcops_server.tasks.bmi import Bmi 

565 

566 patient = self.create_patient_with_idnum_1001() 

567 self.task = Bmi() 

568 self.apply_standard_task_fields(self.task) 

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

570 self.task.height_m = 1.83 

571 self.task.mass_kg = 67.57 

572 self.task.patient_id = patient.id 

573 self.dbsession.add(self.task) 

574 self.dbsession.commit() 

575 

576 def test_record_exported(self) -> None: 

577 from camcops_server.cc_modules.cc_exportmodels import ( 

578 ExportedTask, 

579 ExportedTaskRedcap, 

580 ) 

581 

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

583 exported_task_redcap = ExportedTaskRedcap(exported_task) 

584 

585 exporter = MockRedcapTaskExporter() 

586 project = exporter.get_project() 

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

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

589 project.export_project_info.return_value = { 

590 "record_autonumbering_enabled": 1 

591 } 

592 

593 exporter.export_task(self.req, exported_task_redcap) 

594 self.assertEqual(exported_task_redcap.redcap_record_id, "123") 

595 self.assertEqual(exported_task_redcap.redcap_instrument_name, "bmi") 

596 self.assertEqual(exported_task_redcap.redcap_instance_id, 1) 

597 

598 args, kwargs = project.export_records.call_args 

599 

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

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

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

603 

604 # Initial call with original record 

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

606 

607 rows = args[0] 

608 record = rows[0] 

609 

610 self.assertEqual(record["redcap_repeat_instrument"], "bmi") 

611 self.assertEqual(record["redcap_repeat_instance"], 1) 

612 self.assertEqual(record["record_id"], "0") 

613 self.assertEqual( 

614 record["bmi_complete"], RedcapRecordStatus.COMPLETE.value 

615 ) 

616 self.assertEqual(record["bmi_date"], "2010-07-07") 

617 

618 self.assertEqual(record["pa_height"], "1.8") 

619 self.assertEqual(record["pa_weight"], "67.6") 

620 

621 self.assertEqual(kwargs["return_content"], "auto_ids") 

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

623 

624 # Second call with updated patient ID 

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

626 rows = args[0] 

627 record = rows[0] 

628 

629 self.assertEqual(record["patient_id"], 555) 

630 

631 def test_record_exported_with_non_integer_id(self) -> None: 

632 from camcops_server.cc_modules.cc_exportmodels import ( 

633 ExportedTask, 

634 ExportedTaskRedcap, 

635 ) 

636 

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

638 exported_task_redcap = ExportedTaskRedcap(exported_task) 

639 

640 exporter = MockRedcapTaskExporter() 

641 project = exporter.get_project() 

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

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

644 project.export_project_info.return_value = { 

645 "record_autonumbering_enabled": 1 

646 } 

647 

648 exporter.export_task(self.req, exported_task_redcap) 

649 self.assertEqual(exported_task_redcap.redcap_record_id, "15-123") 

650 

651 def test_record_id_generated_when_no_autonumbering(self) -> None: 

652 from camcops_server.cc_modules.cc_exportmodels import ( 

653 ExportedTask, 

654 ExportedTaskRedcap, 

655 ) 

656 

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

658 exported_task_redcap = ExportedTaskRedcap(exported_task) 

659 

660 exporter = MockRedcapTaskExporter() 

661 project = exporter.get_project() 

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

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

664 project.export_project_info.return_value = { 

665 "record_autonumbering_enabled": 0 

666 } 

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

668 

669 exporter.export_task(self.req, exported_task_redcap) 

670 

671 # Initial call with original record 

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

673 

674 rows = args[0] 

675 record = rows[0] 

676 

677 self.assertEqual(record["record_id"], "15-29") 

678 self.assertEqual(kwargs["return_content"], "count") 

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

680 

681 def test_record_imported_when_no_existing_records(self) -> None: 

682 from camcops_server.cc_modules.cc_exportmodels import ( 

683 ExportedTask, 

684 ExportedTaskRedcap, 

685 ) 

686 

687 exporter = MockRedcapTaskExporter() 

688 project = exporter.get_project() 

689 project.export_records.return_value = DataFrame() 

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

691 project.export_project_info.return_value = { 

692 "record_autonumbering_enabled": 1 

693 } 

694 

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

696 exported_task_redcap = ExportedTaskRedcap(exported_task) 

697 exporter.export_task(self.req, exported_task_redcap) 

698 

699 self.assertEqual(exported_task_redcap.redcap_record_id, "1") 

700 self.assertEqual(exported_task_redcap.redcap_instrument_name, "bmi") 

701 self.assertEqual(exported_task_redcap.redcap_instance_id, 1) 

702 

703 

704class BmiRedcapUpdateTests(BmiRedcapValidFieldmapTestCase): 

705 def create_tasks(self) -> None: 

706 from camcops_server.tasks.bmi import Bmi 

707 

708 patient = self.create_patient_with_idnum_1001() 

709 self.task1 = Bmi() 

710 self.apply_standard_task_fields(self.task1) 

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

712 self.task1.height_m = 1.83 

713 self.task1.mass_kg = 67.57 

714 self.task1.patient_id = patient.id 

715 self.dbsession.add(self.task1) 

716 

717 self.task2 = Bmi() 

718 self.apply_standard_task_fields(self.task2) 

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

720 self.task2.height_m = 1.83 

721 self.task2.mass_kg = 68.5 

722 self.task2.patient_id = patient.id 

723 self.dbsession.add(self.task2) 

724 self.dbsession.commit() 

725 

726 def test_existing_record_id_used_for_update(self) -> None: 

727 from camcops_server.cc_modules.cc_exportmodels import ( 

728 ExportedTask, 

729 ExportedTaskRedcap, 

730 ) 

731 

732 exporter = MockRedcapTaskExporter() 

733 project = exporter.get_project() 

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

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

736 project.export_project_info.return_value = { 

737 "record_autonumbering_enabled": 1 

738 } 

739 

740 exported_task1 = ExportedTask( 

741 task=self.task1, recipient=self.recipient 

742 ) 

743 exported_task_redcap1 = ExportedTaskRedcap(exported_task1) 

744 exporter.export_task(self.req, exported_task_redcap1) 

745 self.assertEqual(exported_task_redcap1.redcap_record_id, "123") 

746 self.assertEqual(exported_task_redcap1.redcap_instrument_name, "bmi") 

747 self.assertEqual(exported_task_redcap1.redcap_instance_id, 1) 

748 

749 project.export_records.return_value = DataFrame( 

750 { 

751 "record_id": ["123"], 

752 "patient_id": [555], 

753 "redcap_repeat_instrument": ["bmi"], 

754 "redcap_repeat_instance": [1], 

755 } 

756 ) 

757 exported_task2 = ExportedTask( 

758 task=self.task2, recipient=self.recipient 

759 ) 

760 exported_task_redcap2 = ExportedTaskRedcap(exported_task2) 

761 

762 exporter.export_task(self.req, exported_task_redcap2) 

763 self.assertEqual(exported_task_redcap2.redcap_record_id, "123") 

764 self.assertEqual(exported_task_redcap2.redcap_instrument_name, "bmi") 

765 self.assertEqual(exported_task_redcap2.redcap_instance_id, 2) 

766 

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

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

769 

770 rows = args[0] 

771 record = rows[0] 

772 

773 self.assertEqual(record["record_id"], "123") 

774 self.assertEqual(record["redcap_repeat_instance"], 2) 

775 self.assertEqual(kwargs["return_content"], "count") 

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

777 

778 

779class Phq9RedcapExportTests(RedcapExportTestCase): 

780 """ 

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

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

783 field. 

784 """ 

785 

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

787<fieldmap> 

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

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

790 <instruments> 

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

792 <fields> 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

807 </fields> 

808 </instrument> 

809 </instruments> 

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

811 

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

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

814 self.id_sequence = self.get_id() 

815 

816 @staticmethod 

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

818 i = 1 

819 

820 while True: 

821 yield i 

822 i += 1 

823 

824 def create_tasks(self) -> None: 

825 from camcops_server.tasks.phq9 import Phq9 

826 

827 patient = self.create_patient_with_idnum_1001() 

828 self.task = Phq9() 

829 self.apply_standard_task_fields(self.task) 

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

831 self.task.q1 = 0 

832 self.task.q2 = 1 

833 self.task.q3 = 2 

834 self.task.q4 = 3 

835 self.task.q5 = 0 

836 self.task.q6 = 1 

837 self.task.q7 = 2 

838 self.task.q8 = 3 

839 self.task.q9 = 0 

840 self.task.q10 = 3 

841 self.task.patient_id = patient.id 

842 self.dbsession.add(self.task) 

843 self.dbsession.commit() 

844 

845 def test_record_exported(self) -> None: 

846 from camcops_server.cc_modules.cc_exportmodels import ( 

847 ExportedTask, 

848 ExportedTaskRedcap, 

849 ) 

850 

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

852 exported_task_redcap = ExportedTaskRedcap(exported_task) 

853 

854 exporter = MockRedcapTaskExporter() 

855 project = exporter.get_project() 

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

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

858 project.export_project_info.return_value = { 

859 "record_autonumbering_enabled": 1 

860 } 

861 

862 exporter.export_task(self.req, exported_task_redcap) 

863 self.assertEqual(exported_task_redcap.redcap_record_id, "123") 

864 self.assertEqual( 

865 exported_task_redcap.redcap_instrument_name, 

866 "patient_health_questionnaire_9", 

867 ) 

868 self.assertEqual(exported_task_redcap.redcap_instance_id, 1) 

869 

870 # Initial call with new record 

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

872 

873 rows = args[0] 

874 record = rows[0] 

875 

876 self.assertEqual( 

877 record["redcap_repeat_instrument"], 

878 "patient_health_questionnaire_9", 

879 ) 

880 self.assertEqual(record["my_record_id"], "0") 

881 self.assertEqual( 

882 record["patient_health_questionnaire_9_complete"], 

883 RedcapRecordStatus.COMPLETE.value, 

884 ) 

885 self.assertEqual(record["phq9_how_difficult"], 4) 

886 self.assertEqual(record["phq9_total_score"], 12) 

887 self.assertEqual(record["phq9_first_name"], "Forename2") 

888 self.assertEqual(record["phq9_last_name"], "Surname2") 

889 self.assertEqual(record["phq9_date_enrolled"], "2010-07-07") 

890 

891 self.assertEqual(record["phq9_1"], 0) 

892 self.assertEqual(record["phq9_2"], 1) 

893 self.assertEqual(record["phq9_3"], 2) 

894 self.assertEqual(record["phq9_4"], 3) 

895 self.assertEqual(record["phq9_5"], 0) 

896 self.assertEqual(record["phq9_6"], 1) 

897 self.assertEqual(record["phq9_7"], 2) 

898 self.assertEqual(record["phq9_8"], 3) 

899 self.assertEqual(record["phq9_9"], 0) 

900 

901 self.assertEqual(kwargs["return_content"], "auto_ids") 

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

903 

904 # Second call with patient ID 

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

906 

907 rows = args[0] 

908 record = rows[0] 

909 self.assertEqual(record["patient_id"], 555) 

910 

911 

912class MedicationTherapyRedcapExportTests(RedcapExportTestCase): 

913 """ 

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

915 related to the KhandakerMojoMedicationTherapy task 

916 """ 

917 

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

919<fieldmap> 

920 <event name="event_1_arm_1" /> 

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

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

923 <instruments> 

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

925 <files> 

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

927 </files> 

928 </instrument> 

929 </instruments> 

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

931 

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

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

934 self.id_sequence = self.get_id() 

935 

936 @staticmethod 

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

938 i = 1 

939 

940 while True: 

941 yield i 

942 i += 1 

943 

944 def create_tasks(self) -> None: 

945 from camcops_server.tasks.khandaker_mojo_medicationtherapy import ( 

946 KhandakerMojoMedicationTherapy, 

947 ) 

948 

949 patient = self.create_patient_with_idnum_1001() 

950 self.task = KhandakerMojoMedicationTherapy() 

951 self.apply_standard_task_fields(self.task) 

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

953 self.task.patient_id = patient.id 

954 self.dbsession.add(self.task) 

955 self.dbsession.commit() 

956 

957 def test_record_exported(self) -> None: 

958 from camcops_server.cc_modules.cc_exportmodels import ( 

959 ExportedTask, 

960 ExportedTaskRedcap, 

961 ) 

962 

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

964 exported_task_redcap = ExportedTaskRedcap(exported_task) 

965 

966 exporter = MockRedcapTaskExporter() 

967 project = exporter.get_project() 

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

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

970 project.export_project_info.return_value = { 

971 "record_autonumbering_enabled": 1 

972 } 

973 

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

975 # the file will already have been closed by then 

976 # noinspection PyUnusedLocal 

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

978 # record, field, fname, fobj 

979 file_obj = import_file_args[3] 

980 read_pdf_bytes.pdf_header = file_obj.read(5) 

981 

982 project.import_file.side_effect = read_pdf_bytes 

983 

984 exporter.export_task(self.req, exported_task_redcap) 

985 self.assertEqual(exported_task_redcap.redcap_record_id, "123") 

986 self.assertEqual( 

987 exported_task_redcap.redcap_instrument_name, "medication_table" 

988 ) 

989 self.assertEqual(exported_task_redcap.redcap_instance_id, 1) 

990 

991 args, kwargs = project.import_file.call_args 

992 

993 record_id = args[0] 

994 fieldname = args[1] 

995 filename = args[2] 

996 

997 self.assertEqual(record_id, "123") 

998 self.assertEqual(fieldname, "medtbl_medication_items") 

999 self.assertEqual( 

1000 filename, 

1001 "khandaker_mojo_medicationtherapy_123_medtbl_medication_items", 

1002 ) 

1003 

1004 self.assertEqual(kwargs["repeat_instance"], 1) 

1005 # noinspection PyUnresolvedReferences 

1006 self.assertEqual(read_pdf_bytes.pdf_header, b"%PDF-") 

1007 self.assertEqual(kwargs["event"], "event_1_arm_1") 

1008 

1009 

1010class MultipleTaskRedcapExportTests(RedcapExportTestCase): 

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

1012<fieldmap> 

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

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

1015 <instruments> 

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

1017 <fields> 

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

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

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

1021 </fields> 

1022 </instrument> 

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

1024 <files> 

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

1026 </files> 

1027 </instrument> 

1028 </instruments> 

1029</fieldmap> 

1030""" # noqa: E501 

1031 

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

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

1034 self.id_sequence = self.get_id() 

1035 

1036 @staticmethod 

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

1038 i = 1 

1039 

1040 while True: 

1041 yield i 

1042 i += 1 

1043 

1044 def create_tasks(self) -> None: 

1045 from camcops_server.tasks.khandaker_mojo_medicationtherapy import ( 

1046 KhandakerMojoMedicationTherapy, 

1047 ) 

1048 

1049 patient = self.create_patient_with_idnum_1001() 

1050 self.mojo_task = KhandakerMojoMedicationTherapy() 

1051 self.apply_standard_task_fields(self.mojo_task) 

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

1053 self.mojo_task.patient_id = patient.id 

1054 self.dbsession.add(self.mojo_task) 

1055 self.dbsession.commit() 

1056 

1057 from camcops_server.tasks.bmi import Bmi 

1058 

1059 self.bmi_task = Bmi() 

1060 self.apply_standard_task_fields(self.bmi_task) 

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

1062 self.bmi_task.height_m = 1.83 

1063 self.bmi_task.mass_kg = 67.57 

1064 self.bmi_task.patient_id = patient.id 

1065 self.dbsession.add(self.bmi_task) 

1066 self.dbsession.commit() 

1067 

1068 def test_instance_ids_on_different_tasks_in_same_record(self) -> None: 

1069 from camcops_server.cc_modules.cc_exportmodels import ( 

1070 ExportedTask, 

1071 ExportedTaskRedcap, 

1072 ) 

1073 

1074 exporter = MockRedcapTaskExporter() 

1075 project = exporter.get_project() 

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

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

1078 project.export_project_info.return_value = { 

1079 "record_autonumbering_enabled": 1 

1080 } 

1081 

1082 exported_task_mojo = ExportedTask( 

1083 task=self.mojo_task, recipient=self.recipient 

1084 ) 

1085 exported_task_redcap_mojo = ExportedTaskRedcap(exported_task_mojo) 

1086 exporter.export_task(self.req, exported_task_redcap_mojo) 

1087 self.assertEqual(exported_task_redcap_mojo.redcap_record_id, "123") 

1088 args, kwargs = project.import_file.call_args 

1089 

1090 self.assertEqual(kwargs["repeat_instance"], 1) 

1091 

1092 project.export_records.return_value = DataFrame( 

1093 { 

1094 "record_id": ["123"], 

1095 "patient_id": [555], 

1096 "redcap_repeat_instrument": [ 

1097 "khandaker_mojo_medicationtherapy" 

1098 ], 

1099 "redcap_repeat_instance": [1], 

1100 } 

1101 ) 

1102 exported_task_bmi = ExportedTask( 

1103 task=self.bmi_task, recipient=self.recipient 

1104 ) 

1105 exported_task_redcap_bmi = ExportedTaskRedcap(exported_task_bmi) 

1106 

1107 exporter.export_task(self.req, exported_task_redcap_bmi) 

1108 

1109 # Import of second task, but is first instance 

1110 # (third call to import_records) 

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

1112 

1113 rows = args[0] 

1114 record = rows[0] 

1115 

1116 self.assertEqual(record["redcap_repeat_instance"], 1) 

1117 

1118 def test_imported_into_different_events(self) -> None: 

1119 from camcops_server.cc_modules.cc_exportmodels import ( 

1120 ExportedTask, 

1121 ExportedTaskRedcap, 

1122 ) 

1123 

1124 exporter = MockRedcapTaskExporter() 

1125 project = exporter.get_project() 

1126 

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

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

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

1130 project.export_project_info.return_value = { 

1131 "record_autonumbering_enabled": 1 

1132 } 

1133 

1134 exported_task_mojo = ExportedTask( 

1135 task=self.mojo_task, recipient=self.recipient 

1136 ) 

1137 exported_task_redcap_mojo = ExportedTaskRedcap(exported_task_mojo) 

1138 

1139 exporter.export_task(self.req, exported_task_redcap_mojo) 

1140 

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

1142 rows = args[0] 

1143 record = rows[0] 

1144 

1145 self.assertEqual(record["redcap_event_name"], "mojo_event") 

1146 args, kwargs = project.import_file.call_args 

1147 

1148 self.assertEqual(kwargs["event"], "mojo_event") 

1149 

1150 exported_task_bmi = ExportedTask( 

1151 task=self.bmi_task, recipient=self.recipient 

1152 ) 

1153 exported_task_redcap_bmi = ExportedTaskRedcap(exported_task_bmi) 

1154 

1155 exporter.export_task(self.req, exported_task_redcap_bmi) 

1156 

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

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

1159 rows = args[0] 

1160 record = rows[0] 

1161 self.assertEqual(record["redcap_event_name"], "bmi_event") 

1162 

1163 

1164class BadConfigurationRedcapTests(RedcapExportTestCase): 

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

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

1167 self.id_sequence = self.get_id() 

1168 

1169 @staticmethod 

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

1171 i = 1 

1172 

1173 while True: 

1174 yield i 

1175 i += 1 

1176 

1177 def create_tasks(self) -> None: 

1178 from camcops_server.tasks.bmi import Bmi 

1179 

1180 patient = self.create_patient_with_idnum_1001() 

1181 self.task = Bmi() 

1182 self.apply_standard_task_fields(self.task) 

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

1184 self.task.height_m = 1.83 

1185 self.task.mass_kg = 67.57 

1186 self.task.patient_id = patient.id 

1187 self.dbsession.add(self.task) 

1188 self.dbsession.commit() 

1189 

1190 

1191class MissingInstrumentRedcapTests(BadConfigurationRedcapTests): 

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

1193<fieldmap> 

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

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

1196 <instruments> 

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

1198 <fields> 

1199 </fields> 

1200 </instrument> 

1201 </instruments> 

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

1203 

1204 def test_raises_when_instrument_missing_from_fieldmap(self) -> None: 

1205 from camcops_server.cc_modules.cc_exportmodels import ( 

1206 ExportedTask, 

1207 ExportedTaskRedcap, 

1208 ) 

1209 

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

1211 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1212 

1213 exporter = MockRedcapTaskExporter() 

1214 project = exporter.get_project() 

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

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

1217 

1218 with self.assertRaises(RedcapExportException) as cm: 

1219 exporter.export_task(self.req, exported_task_redcap) 

1220 

1221 message = str(cm.exception) 

1222 self.assertIn( 

1223 "Instrument for task 'bmi' is missing from the fieldmap", message 

1224 ) 

1225 

1226 

1227class IncorrectRecordIdRedcapTests(BadConfigurationRedcapTests): 

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

1229<fieldmap> 

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

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

1232 <instruments> 

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

1234 <fields> 

1235 </fields> 

1236 </instrument> 

1237 </instruments> 

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

1239 

1240 def test_raises_when_record_id_is_incorrect(self) -> None: 

1241 from camcops_server.cc_modules.cc_exportmodels import ( 

1242 ExportedTask, 

1243 ExportedTaskRedcap, 

1244 ) 

1245 

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

1247 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1248 

1249 exporter = MockRedcapTaskExporter() 

1250 project = exporter.get_project() 

1251 project.export_records.return_value = DataFrame( 

1252 { 

1253 "record_id": ["123"], 

1254 "patient_id": [555], 

1255 "redcap_repeat_instrument": ["bmi"], 

1256 "redcap_repeat_instance": [1], 

1257 } 

1258 ) 

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

1260 project.export_project_info.return_value = { 

1261 "record_autonumbering_enabled": 1 

1262 } 

1263 

1264 with self.assertRaises(RedcapExportException) as cm: 

1265 exporter.export_task(self.req, exported_task_redcap) 

1266 

1267 message = str(cm.exception) 

1268 self.assertIn("Field 'my_record_id' does not exist in REDCap", message) 

1269 

1270 

1271class IncorrectPatientIdRedcapTests(BadConfigurationRedcapTests): 

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

1273<fieldmap> 

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

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

1276 <instruments> 

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

1278 <fields> 

1279 </fields> 

1280 </instrument> 

1281 </instruments> 

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

1283 

1284 def test_raises_when_patient_id_is_incorrect(self) -> None: 

1285 from camcops_server.cc_modules.cc_exportmodels import ( 

1286 ExportedTask, 

1287 ExportedTaskRedcap, 

1288 ) 

1289 

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

1291 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1292 

1293 exporter = MockRedcapTaskExporter() 

1294 project = exporter.get_project() 

1295 project.export_records.return_value = DataFrame( 

1296 { 

1297 "record_id": ["123"], 

1298 "patient_id": [555], 

1299 "redcap_repeat_instrument": ["bmi"], 

1300 "redcap_repeat_instance": [1], 

1301 } 

1302 ) 

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

1304 project.export_project_info.return_value = { 

1305 "record_autonumbering_enabled": 1 

1306 } 

1307 

1308 with self.assertRaises(RedcapExportException) as cm: 

1309 exporter.export_task(self.req, exported_task_redcap) 

1310 

1311 message = str(cm.exception) 

1312 self.assertIn( 

1313 "Field 'my_patient_id' does not exist in REDCap", message 

1314 ) 

1315 

1316 

1317class MissingPatientInstrumentRedcapTests(BadConfigurationRedcapTests): 

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

1319<fieldmap> 

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

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

1322 <instruments> 

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

1324 <fields> 

1325 </fields> 

1326 </instrument> 

1327 </instruments> 

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

1329 

1330 def test_raises_when_instrument_is_missing(self) -> None: 

1331 from camcops_server.cc_modules.cc_exportmodels import ( 

1332 ExportedTask, 

1333 ExportedTaskRedcap, 

1334 ) 

1335 

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

1337 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1338 

1339 exporter = MockRedcapTaskExporter() 

1340 project = exporter.get_project() 

1341 project.export_records.side_effect = redcap.RedcapError( 

1342 "Something went wrong" 

1343 ) 

1344 

1345 with self.assertRaises(RedcapExportException) as cm: 

1346 exporter.export_task(self.req, exported_task_redcap) 

1347 

1348 message = str(cm.exception) 

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

1350 

1351 

1352class MissingEventRedcapTests(BadConfigurationRedcapTests): 

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

1354<fieldmap> 

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

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

1357 <instruments> 

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

1359 <fields> 

1360 </fields> 

1361 </instrument> 

1362 </instruments> 

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

1364 

1365 def test_raises_for_longitudinal_project(self) -> None: 

1366 from camcops_server.cc_modules.cc_exportmodels import ( 

1367 ExportedTask, 

1368 ExportedTaskRedcap, 

1369 ) 

1370 

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

1372 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1373 

1374 exporter = MockRedcapTaskExporter() 

1375 project = exporter.get_project() 

1376 

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

1378 

1379 with self.assertRaises(RedcapExportException) as cm: 

1380 exporter.export_task(self.req, exported_task_redcap) 

1381 

1382 message = str(cm.exception) 

1383 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message) 

1384 

1385 

1386class MissingInstrumentEventRedcapTests(BadConfigurationRedcapTests): 

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

1388<fieldmap> 

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

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

1391 <instruments> 

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

1393 <fields> 

1394 </fields> 

1395 </instrument> 

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

1397 <fields> 

1398 </fields> 

1399 </instrument> 

1400 </instruments> 

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

1402 

1403 def test_raises_when_instrument_missing_event(self) -> None: 

1404 from camcops_server.cc_modules.cc_exportmodels import ( 

1405 ExportedTask, 

1406 ExportedTaskRedcap, 

1407 ) 

1408 

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

1410 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1411 

1412 exporter = MockRedcapTaskExporter() 

1413 project = exporter.get_project() 

1414 

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

1416 

1417 with self.assertRaises(RedcapExportException) as cm: 

1418 exporter.export_task(self.req, exported_task_redcap) 

1419 

1420 message = str(cm.exception) 

1421 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message) 

1422 

1423 

1424class AnonymousTaskRedcapTests(RedcapExportTestCase): 

1425 def create_tasks(self) -> None: 

1426 from camcops_server.tasks.apeq_cpft_perinatal import APEQCPFTPerinatal 

1427 

1428 self.task = APEQCPFTPerinatal() 

1429 self.apply_standard_task_fields(self.task) 

1430 self.task.id = 1 

1431 self.dbsession.add(self.task) 

1432 self.dbsession.commit() 

1433 

1434 def test_raises_when_task_is_anonymous(self) -> None: 

1435 from camcops_server.cc_modules.cc_exportmodels import ( 

1436 ExportedTask, 

1437 ExportedTaskRedcap, 

1438 ) 

1439 

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

1441 exported_task_redcap = ExportedTaskRedcap(exported_task) 

1442 

1443 exporter = MockRedcapTaskExporter() 

1444 

1445 with self.assertRaises(RedcapExportException) as cm: 

1446 exporter.export_task(self.req, exported_task_redcap) 

1447 

1448 message = str(cm.exception) 

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